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
12 changes: 9 additions & 3 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@

### 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 default to a 1 MiB limit.** A larger entry is refused
with 413. Set `webservices.atomPubMaxEntrySize` in Server Settings to change
the limit in bytes (default 1048576). Media uploads use the existing file
upload limits.
- **Templates can no longer reach the objects behind the template wrappers.**
`$weblog.pojo`, `$entry.pojo` and `getPojo()` no longer resolve in weblog
templates. A custom theme that uses them will print the reference text
Expand Down Expand Up @@ -63,15 +69,15 @@
site, `/planetrss` printed `$utils.escapeXML($siteName)` as its title and
description, and logged a warning for each. Unsaved Planet settings now use
their defaults.
- **An image pasted into the rich text editor appears once.** Pasting an image
copied from a web page inserted it twice.
- **Decimal settings can be saved on the configuration page.** The maximum
upload file and directory sizes accepted only whole numbers in the browser,
although they are measured in megabytes with decimals (default `2.00`).

## 6.1.6

Initial installation now requires a one-time, cryptographically secure setup token printed to the server log. Bootstrap access closes as soon as setup finishes — when the first administrator is created on a new site, or when the database upgrade completes on an existing one.
Initial installation now requires a one-time, cryptographically secure setup token printed to the server log.
Bootstrap access closes as soon as setup finishes — when the first administrator is created on a new site,
or when the database upgrade completes on an existing one.

A maintenance release. Users of 6.1.5 and earlier are encouraged to upgrade.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,12 @@ public String save() {
} else if ( incomingProp != null && propertyDef.getType().equals("integer") ) {

try {
Integer.parseInt(incomingProp);
int value = Integer.parseInt(incomingProp);
if ("webservices.atomPubMaxEntrySize".equals(propName)
&& (value <= 0 || value == Integer.MAX_VALUE)) {
addError("ConfigForm.invalidAtomPubMaxEntrySize");
continue;
}
updProp.setValue(incomingProp);
log.debug("Set integer " + propName + " = " + incomingProp);
} catch ( NumberFormatException nfe ) {
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,237 @@
/*
* 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.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import javax.xml.parsers.ParserConfigurationException;
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.SecureXmlParsers;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
* 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);

/** Default maximum Atom entry body size, in bytes. Media uploads are not affected. */
static final int DEFAULT_MAX_ENTRY_BYTES = 1024 * 1024;

static final String MAX_ENTRY_SIZE_PROPERTY = "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);

int maxEntryBytes = maxEntryBytes();
// Read one byte past the limit, so an oversized body can be detected.
byte[] body = req.getInputStream().readNBytes(maxEntryBytes + 1);
if (body.length > maxEntryBytes) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}
DefaultHandler contentHandler = new DefaultHandler() {
@Override
public void error(SAXParseException e) throws SAXException {
throw e;
}
};
XMLReader reader;
try {
reader = SecureXmlParsers.newSAXParserFactory().newSAXParser().getXMLReader();

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.

Nitpick: I would still reject DOCTYPE explicitly here either with a LexicalHandler (for a personalized exception and/or Android compatibility) or disable-doctype-decl.

The contract of SecureXmlParsers.newSAXParserFactory() does not need to include DOCTYPE rejection: any other hardening measure is good enough. In this particular case we need to reject it to workaround a problem in an upstream EOL-ed artifact.

Suggested change
reader = SecureXmlParsers.newSAXParserFactory().newSAXParser().getXMLReader();
reader = SecureXmlParsers.newSAXParserFactory().newSAXParser().getXMLReader();
// Hardening: DOCTYPE declarations should be rejected,
// regardless whether the secure reader already does it.
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");
}
};
parser.setProperty("http://xml.org/sax/properties/lexical-handler", doctypeRefuser);

} catch (ParserConfigurationException | SAXException e) {
throw new ServletException("Could not create an Atom entry parser", e);
}
reader.setContentHandler(contentHandler);
reader.setErrorHandler(contentHandler);
try {
// Propono reads the entry as UTF-8 text, so check the same text.
reader.parse(new InputSource(new InputStreamReader(
new ByteArrayInputStream(body), StandardCharsets.UTF_8)));
} catch (SAXException e) {
LOG.debug("Rejecting Atom entry that could not be parsed", e);
sendText(res, HttpServletResponse.SC_BAD_REQUEST, "Invalid Atom entry");
return;
}
Comment thread
snoopdave marked this conversation as resolved.
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;
}

/** Uses the default when an older installation has no setting or its value is invalid. */
private static int maxEntryBytes() {
String value = WebloggerRuntimeConfig.getProperty(MAX_ENTRY_SIZE_PROPERTY);
if (value != null) {
try {
int limit = Integer.parseInt(value.trim());
if (limit > 0 && limit < Integer.MAX_VALUE) {
return limit;
}
} catch (NumberFormatException e) {
// Fall back to the default below.
}
LOG.warn("Invalid " + MAX_ENTRY_SIZE_PROPERTY + "; using the default entry limit");
}
return DEFAULT_MAX_ENTRY_BYTES;
}

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: 2 additions & 0 deletions app/src/main/resources/ApplicationResources.properties
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ configForm.editorPages=Editor Pages
configForm.webServicesSettings=Web Services Settings
configForm.enableAtomPub=Enable Atom Publishing Protocol
configForm.AtomPubAuth=AtomPub authentication (basic or oauth)
configForm.atomPubMaxEntrySize=Maximum AtomPub entry size (bytes)
configForm.enableXmlRpc=Enable Blogger / MetaWeblog API

configForm.weblogSettings=Weblog Rendering Settings
Expand Down Expand Up @@ -1135,6 +1136,7 @@ ConfigForm.error.saveFailed=Error saving Planet configuration

ConfigForm.invalidBooleanProperty=Property {0} must be a boolean: {1}
ConfigForm.invalidIntegerProperty=Property {0} must be an integer: {1}
ConfigForm.invalidAtomPubMaxEntrySize=Maximum AtomPub entry size must be between 1 and 2147483646 bytes.
ConfigForm.invalidFloatProperty=Property {0} must be a float: {1}
ConfigForm.invalidProperty=Property {0} is null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
<default-value>basic</default-value>
</property-def>

<property-def name="webservices.atomPubMaxEntrySize" key="configForm.atomPubMaxEntrySize">
<type>integer</type>
<default-value>1048576</default-value>
</property-def>

</display-group>

<!-- Weblog Rendering Settings Group -->
Expand Down
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