Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Behaviour changes worth reading before upgrading

- **AtomPub honours `webservices.enableAtomPub` on every request.** While the
setting is off, every AtomPub URL answers 404, not only the service document.
- **AtomPub entry bodies are limited to 10 MB.** A larger entry is refused with
413. Media uploads are not affected.
- **XML parsing uses Apache Commons Secure XML.** Roller now bundles
`commons-secure-xml` 1.0.0 and builds all of its XML parsers through it.
- **Startup fails if the XML-RPC parser cannot be configured.** Roller used to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,18 @@ public boolean isAtomServiceURI(AtomRequest areq) {
*/
@Override
public boolean isEntryURI(AtomRequest areq) {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
return isEntryPath(areq.getPathInfo());
}

/**
* True if the path info names an entry. Shared with RollerAtomServlet so
* both agree on which requests carry an entry body.
*/
static boolean isEntryPath(String path) {
String[] pathInfo = StringUtils.split(path, "/");
if (pathInfo == null) {
return false;
}
if (pathInfo.length > 2 && pathInfo[1].equals("entry")) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@
public class RollerAtomHandlerFactory extends AtomHandlerFactory {

/**
* Create new AtomHandler.
* Return the handler that {@link RollerAtomServlet} already authenticated
* for this request, or create a new AtomHandler.
*/
@Override
public AtomHandler newAtomHandler(
HttpServletRequest req, HttpServletResponse res) {
Object handler = req.getAttribute(RollerAtomServlet.HANDLER_ATTRIBUTE);
if (handler instanceof AtomHandler) {
req.removeAttribute(RollerAtomServlet.HANDLER_ATTRIBUTE);
return (AtomHandler) handler;
}
return new RollerAtomHandler(req, res);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. The ASF licenses this file to You
* under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. For additional information regarding
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/

package org.apache.roller.weblogger.webservices.atomprotocol;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import com.rometools.propono.atom.server.AtomHandler;
import com.rometools.propono.atom.server.AtomServlet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.config.WebloggerRuntimeConfig;
import org.apache.roller.weblogger.util.SafeSAXBuilder;
import org.jdom2.JDOMException;

/**
* Roller's AtomPub endpoint. It answers only while
* <code>webservices.enableAtomPub</code> is on, and it reads each Atom entry
* body with Roller's shared XML parser settings before the Propono servlet
* handles the request.
*/
public class RollerAtomServlet extends AtomServlet {

private static final long serialVersionUID = 1L;

private static final Log LOG = LogFactory.getLog(RollerAtomServlet.class);

/** Largest Atom entry body accepted, in bytes. Media uploads are not affected. */
static final int MAX_ENTRY_BYTES = 10 * 1024 * 1024;
Comment on lines +55 to +56

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't this limit a little high form one Atom entry? The entry still need to fit in the database. Even counting for the overhead of XML, 128 KiB or 1 MiB should be enough for all users.

This value might also be configurable (webservices.atomPubMaxEntrySize?).


private static final String ATOM_CONTENT_TYPE = "application/atom+xml";

/**
* Request attribute that carries the handler authenticated by this servlet
* to {@link RollerAtomHandlerFactory}, so Propono does not authenticate the
* request a second time.
*/
static final String HANDLER_ATTRIBUTE = RollerAtomServlet.class.getName() + ".handler";

@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {

if (!WebloggerRuntimeConfig.getBooleanProperty("webservices.enableAtomPub")) {
LOG.debug("AtomPub service is disabled; rejecting request");
sendText(res, HttpServletResponse.SC_NOT_FOUND, "AtomPub service is disabled");
return;
}

if (!carriesEntry(req)) {
forward(req, res);
return;
}

// Authenticate before reading the body, as Propono does.
AtomHandler handler = createHandler(req, res);
if (handler.getAuthenticatedUsername() == null) {
res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
req.setAttribute(HANDLER_ATTRIBUTE, handler);

byte[] body = readBody(req.getInputStream());
Comment thread
snoopdave marked this conversation as resolved.
if (body == null) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}
Comment on lines +91 to +95

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You already have commons-io as a transitive dependency. You could use it to replace readBody:

Suggested change
byte[] body = readBody(req.getInputStream());
if (body == null) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}
// Read one byte past the limit, so an oversized body can be detected.
byte[] body = IOUtils.toByteArray(
BoundedInputStream.builder()
.setInputStream(req.getInputStream())
.setMaxCount(MAX_ENTRY_BYTES + 1L)
.get());
if (body.length > MAX_ENTRY_BYTES) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}

In Java 9+ you also have InputStream.readNBytes()

try {
// Propono reads the entry as UTF-8 text, so check the same text.
SafeSAXBuilder saxBuilder = new SafeSAXBuilder();
saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxBuilder.build(new InputStreamReader(
new ByteArrayInputStream(body), StandardCharsets.UTF_8));
} catch (JDOMException e) {
LOG.debug("Rejecting Atom entry that could not be parsed", e);
sendText(res, HttpServletResponse.SC_BAD_REQUEST, "Invalid Atom entry");
return;
}
Comment on lines +96 to +108

@ppkarwasz ppkarwasz Oct 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

JDOM builds a tree that is later discarded. You might use a SAXParser instead with an empty content handler. The following code uses Commons Secure XML 1.1.0 (which will be released today) to validate the stream:

        try {
            DefaultHandler2 doctypeRefuser = new DefaultHandler2() {
                @Override
                public void startDTD(String name, String publicId, String systemId)
                        throws SAXException {
                    throw new SAXException("DOCTYPE is not allowed in an Atom entry");
                }
            };
            SAXParser parser = SecureSAXParserFactory.newNSSAXParser();
            parser.setProperty("http://xml.org/sax/properties/lexical-handler", doctypeRefuser);
            parser.parse(new InputSource(new InputStreamReader(
                    new ByteArrayInputStream(body), StandardCharsets.UTF_8)), doctypeRefuser);
        } catch (SAXException e) {
            LOG.debug("Rejecting Atom entry that could not be parsed", e);
            sendText(res, HttpServletResponse.SC_BAD_REQUEST, "Invalid Atom entry");
            return;
        }

forward(new BufferedBodyRequest(req, body), res);
}

/** Creates the handler that authenticates the request. */
protected AtomHandler createHandler(HttpServletRequest req, HttpServletResponse res) {
return new RollerAtomHandler(req, res);
}

/** Hands the request to the Propono servlet. */
protected void forward(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
super.service(req, res);
}

/**
* True when Propono would parse the request body as an Atom entry: a POST
* of Atom content, or a PUT to an entry URI.
*/
static boolean carriesEntry(HttpServletRequest req) {
String method = req.getMethod();
if ("POST".equalsIgnoreCase(method)) {
String contentType = req.getContentType();
return contentType != null && contentType.startsWith(ATOM_CONTENT_TYPE);
}
if ("PUT".equalsIgnoreCase(method)) {
return RollerAtomHandler.isEntryPath(req.getPathInfo());
}
return false;
}

/** Reads the whole body, or returns null when it exceeds the limit. */
private static byte[] readBody(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int total = 0;
int read;
while ((read = in.read(buffer)) != -1) {
total += read;
if (total > MAX_ENTRY_BYTES) {
return null;
}
out.write(buffer, 0, read);
}
return out.toByteArray();
}

private static void sendText(HttpServletResponse res, int status, String message)
throws IOException {
res.setStatus(status);
res.setContentType("text/plain;charset=UTF-8");
res.getWriter().write(message);
}

/** A request whose body has already been read into memory. */
static final class BufferedBodyRequest extends HttpServletRequestWrapper {

private final byte[] body;

BufferedBodyRequest(HttpServletRequest request, byte[] body) {
super(request);
this.body = body;
}

@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream in = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return in.read();
}

@Override
public int read(byte[] b, int off, int len) {
return in.read(b, off, len);
}

@Override
public boolean isFinished() {
return in.available() == 0;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
};
}

@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(body), StandardCharsets.UTF_8));
}

@Override
public int getContentLength() {
return body.length;
}

@Override
public long getContentLengthLong() {
return body.length;
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@

<servlet>
<servlet-name>AtomServlet</servlet-name>
<servlet-class>com.rometools.propono.atom.server.AtomServlet</servlet-class>
<servlet-class>org.apache.roller.weblogger.webservices.atomprotocol.RollerAtomServlet</servlet-class>
</servlet>

<servlet>
Expand Down
Loading
Loading