Skip to content

Answer XML-RPC with Roller's own server and drop Apache XML-RPC - #263

Merged
mraible merged 7 commits into
apache:masterfrom
mraible:own-xmlrpc-server
Nov 15, 2026
Merged

mraible merged 7 commits into
apache:masterfrom
mraible:own-xmlrpc-server

Conversation

@mraible

@mraible mraible commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Apache XML-RPC is end of life. 3.1.3, its last release, has an unfixed denial-of-service issue (CVE-2016-5004, the open Dependabot alert). Roller's servlet layer was already a fork, because xmlrpc-server has no Jakarta release. One concrete problem in that fork: it inflated gzip-encoded request bodies with no limit.

This replaces the library with a small server in webservices/xmlrpc. Clients see the same methods, faults and fault codes.

  • XmlRpcRequestParser:
    • parses with SafeSAXBuilder, so DOCTYPEs are refused and nothing is resolved;
    • accepts only the specification's types (string, int/i4, boolean, double, dateTime.iso8601, base64, struct, array) and caps nesting at 32;
    • turns structs into Maps and arrays into Object[] as before, so the handlers' casts are unchanged;
    • reads dates in the spec's yyyyMMddTHH:mm:ss form, plus the dashed, fractional and zoned variants clients send.
  • XmlRpcResponseWriter:
    • writes UTF-8 methodResponse and fault documents, with dates in the old format and the server's zone;
    • escapes markup and drops characters XML 1.0 forbids. That filter now lives in util/XmlCharacters, and AtomWriter uses the same one.
  • XmlRpcServer:
    • exposes each handler's public, non-void methods, inherited ones included, as prefix.method, like the library's property mapping. So metaWeblog.getUsersBlogs still works.
    • picks the overload whose parameter types fit, so the int/boolean publish pairs still work;
    • passes handler faults through with their codes. Any other failure is logged and answered with fault 1000, so internal messages stay on the server.
  • RollerXmlRpcServlet:
    • caps the request at 1 MiB, or while uploads are on, at the largest allowed upload as (line-wrapped) base64 plus 64 KiB;
    • answers 413 above that, and applies the cap to the decompressed size of a gzip body;
    • answers 415 for other content encodings and 400 for broken gzip.

Removed: xmlrpc-common, xmlrpc-server, ws-commons-util, the forked RollerXmlRpcServletServer, XmlRpcServlet.properties, the enabledForExtensions init-param and SecureXmlParsers.installForXmlRpc(). The handlers now throw Roller's own XmlRpcException and XmlRpcNotAuthorizedException.

Tests:

  • XmlRpcServerTest: dispatch, overloads, inherited methods, struct and array conversion, faults, no leaked internals, and that every Blogger and MetaWeblog name is registered.
  • XmlRpcResponseWriterTest: types, dates, escaping, well-formed output and faults.
  • RollerXmlRpcServletTest: both size checks, gzip accepted, a gzip body small on the wire but far over the cap decompressed, bad encodings, and the upload-based cap.
  • XmlRpcExtensionTypeTest: now runs against the new parser.
  • XMLRPCWeblogPermissionTest: passes with only its imports changed.
  • mvn install: 684 tests pass.

I haven't tested this with a desktop blogging client against a running server. That would be a good check before the RC.

Apache XML-RPC is end of life. Its last release, 3.1.3, has an unfixed denial-of-service issue (CVE-2016-5004), and Roller's servlet layer was already a fork because xmlrpc-server has no Jakarta release. It also inflated gzip-encoded request bodies without any limit.

The Blogger and MetaWeblog endpoint now runs on a small server in the xmlrpc package:

- XmlRpcRequestParser reads methodCall documents with SafeSAXBuilder (no DOCTYPE, nothing resolved). It accepts only the specification's value types and caps nesting at 32 levels. Structs become Maps and arrays Object[], as before, so the handlers' casts still hold.
- XmlRpcResponseWriter writes methodResponse and fault documents in UTF-8. Dates use the old yyyyMMddTHH:mm:ss format in the server's zone, and characters XML 1.0 forbids are dropped (the filter now lives in XmlCharacters and is shared with AtomWriter).
- XmlRpcServer exposes each handler's public, non-void methods, inherited ones included, as prefix.method, and picks the overload whose parameter types fit, as the library's property mapping did. Handler faults keep their codes. Any other failure is logged and answered with fault 1000, so internal details aren't sent to the client.
- RollerXmlRpcServlet caps the request at 1 MiB, or at the largest allowed upload as base64 while uploads are on. It answers 413 above that, and the cap also applies to the decompressed size of a gzip body.

xmlrpc-common, xmlrpc-server, ws-commons-util, the forked RollerXmlRpcServletServer, the XmlRpcServlet.properties mapping, the enabledForExtensions init-param and SecureXmlParsers.installForXmlRpc are removed. The handlers now throw Roller's own XmlRpcException and XmlRpcNotAuthorizedException, with the same fault codes.

New tests cover dispatch, overloads, inherited methods, faults, the writer's types and escaping, and the servlet's caps, including a gzip body that is small compressed but far over the cap decompressed. The existing handler permission tests pass unchanged apart from imports.
CodeQL flagged two places on this PR. The parser's build() call was flagged as XXE because CodeQL doesn't follow the settings SafeSAXBuilder makes in its constructor, so the parser now also sets disallow-doctype-decl next to the call; behaviour is unchanged. The servlet's response was flagged for writing an exception message. XmlRpcException now keeps the client-facing fault string as its own field, and the server writes that, which makes clear the text is meant for the client. Unexpected failures still get the generic fault.
… entry

Testing against a copy of raibledesigns.com showed both: the old library wrote each surrogate half of an emoji as its own character reference, so clients rejected the response, and stored carriage returns reached clients normalised to LF.

@snoopdave snoopdave left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR-Review: 1 inline comment posted, plus 1 general issue below.

@snoopdave

Copy link
Copy Markdown
Contributor

PR-Review: General Issues

The following issues were found but cannot be attached to a specific line in the diff:

  • 🐞Claude Issue: Important: No interop test against a real client is recorded. The PR body still says no desktop client was tested against a running server. The latest commit mentions testing against a copy of raibledesigns.com. If that used a real client, please name it here. If not, run at least one before the RC (for example MarsEdit, Open Live Writer, or a python -m xmlrpc.client script that calls metaWeblog.newPost, editPost, getRecentPosts and newMediaObject). Clients often send untyped values, <i8>/<nil/>, dates without seconds or with odd zones, and folded base64.

The cap followed uploads.file.maxsize with no ceiling. The whole body is read into memory and parsed into a document before anyone is authenticated, so a site allowing 100 MB uploads accepted about 137 MB per anonymous request. The cap still rises with the upload size but stops at 32 MiB, which fits a file of about 23 MB as base64. CHANGES and the install guide say so.
@mraible

mraible commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

Both points from the review are addressed.

Request cap. 7edbfb9 stops the cap at 32 MiB whatever uploads.file.maxsize is. It still rises with the upload size below that. A file over about 23 MB no longer fits through a blog client and has to go through the media library; CHANGES and the install guide say so, and there's a test for it.

Client interop. I ran Python 3.15's xmlrpc.client.ServerProxy against a copy of raibledesigns.com (3,553 entries, MySQL 8.4, Tomcat 11), on this branch at f7cb277 and on master at f3d1207 for comparison. No desktop client (MarsEdit, Open Live Writer) was tested.

With the real client, on this branch:

  • blogger.getUsersBlogs, metaWeblog.getCategories and metaWeblog.getRecentPosts return what master returns.
  • metaWeblog.newPost with a draft whose title and body hold an emoji, accented letters and markup, then getPost: the title round-trips exactly, and the body differs only in line endings (the client sends a raw CRLF, which XML parsing turns into LF).
  • metaWeblog.editPost changes the title and category. blogger.deletePost removes the post.
  • metaWeblog.newMediaObject with a 70-byte PNG returns a URL that serves 70 bytes as image/png.

Then the request shapes other clients send, as raw XML:

  • Accepted: untyped <value>text</value> strings, <i4>, whitespace around values, unknown mt_* and wp_* struct members, publish as <int>0</int>, a CDATA title, an emoji as one character reference, a body declared and encoded as ISO-8859-1, a UTF-8 byte order mark, and base64 on one line, folded at 76 with CRLF, or folded at 20 with LF and indentation.
  • Dates accepted: 20261114T12:00:00, 2026-11-14T12:00:00, 20261114T12:00:00Z, 2026-11-14T12:00:00+05:30 (stored as 06:30 on a UTC server), 20261114T12:00:00.123Z and 20261114T120000. Master refuses the dashed, offset and no-colon forms.
  • Refused, as on master: <i8>, <nil/> and <ex:nil/>, a <double> or untyped string where an int is expected, a date without seconds (20261114T12:00), <boolean>false</boolean>, a missing publish parameter, and an emoji written as two surrogate character references.

Two differences from master:

  • A dateCreated sent as an untyped string gets fault 1000 here and fault 0 on master.
  • On master, getPost and getRecentPosts return XML the client can't parse once a post contains an emoji, so the lifecycle above fails there at the first getPost.

@mraible

mraible commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

A correction to my comment above: metaWeblog.getRecentPosts returns what master returns apart from line endings. Posts stored with CRLF come back with \r\n here, where master's raw carriage returns were normalised to \n by the client's XML parser. That's the change already listed in this pull request's CHANGES entry; the other fields are identical.

The request parser caught JDOMException and IOException, but JDOM also refuses some documents with unchecked exceptions: an XML 1.1 document may hold a reference to a control character or a name starting with a supplementary-plane character, which the JDK's parser accepts and JDOM's verifier does not. Those escaped the servlet, so an unauthenticated request got an HTTP 500 with Roller's error page, which prints the exception's message, and a stack trace went to the log each time. Master answers the same requests with a fault.

The parser now turns any failure to build the document into the usual parse fault, and the server answers the generic fault for any other unchecked exception so nothing a request does reaches the container. XmlRpcServerTest covers the XML 1.1 cases and, since nothing did before, that a document type declaration is refused without reading the file it names.

@snoopdave snoopdave left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mraible
mraible merged commit edb3d11 into apache:master Nov 15, 2026
10 checks passed
@mraible
mraible deleted the own-xmlrpc-server branch November 15, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants