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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.util.RollerConstants;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.MediaFileManager;
import org.apache.roller.weblogger.business.WebloggerFactory;
Expand Down Expand Up @@ -116,7 +117,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)

// set the content type based on whatever is in our web.xml mime defs
if (resourceRequest.isThumbnail()) {
response.setContentType("image/png");
MediaTypePolicy.applyResponseHeaders(response, "image/png",
mediaFile.getName());
try {
resourceStream = mediaFile.getThumbnailInputStream();
} catch (Exception e) {
Expand All @@ -131,7 +133,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
}

if (resourceStream == null) {
response.setContentType(mediaFile.getContentType());
MediaTypePolicy.applyResponseHeaders(response,
mediaFile.getContentType(), mediaFile.getName());
resourceStream = mediaFile.getInputStream();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.MediaFileManager;
import org.apache.roller.weblogger.business.WebloggerFactory;
Expand Down Expand Up @@ -129,7 +130,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
}

// if not from theme then see if resource is in weblog's upload dir
boolean fromUploadedMedia = false;
if (resourceStream == null) {
fromUploadedMedia = true;
try {
MediaFileManager mmgr = WebloggerFactory.getWeblogger()
.getMediaFileManager();
Expand Down Expand Up @@ -160,8 +163,19 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
}

// set the content type based on whatever is in our web.xml mime defs
response.setContentType(this.context.getMimeType(resourceRequest
.getResourcePath()));
String resourceType = this.context.getMimeType(
resourceRequest.getResourcePath());
if (fromUploadedMedia) {
// Uploaded through the media library, so it is governed by the
// same policy as any other media response.
MediaTypePolicy.applyResponseHeaders(response, resourceType,
resourceRequest.getResourcePath());
} else {
// A theme resource: authored as part of the theme and served as
// the type the theme intends, but never re-typed by the browser.
response.setHeader("X-Content-Type-Options", "nosniff");
response.setContentType(resourceType);
}

try {
// ok, lets serve up the file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.MediaFileManager;
import org.apache.roller.weblogger.business.WebloggerFactory;
Expand Down Expand Up @@ -125,7 +126,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
}

// if not from theme then see if resource is in weblog's upload dir
boolean fromUploadedMedia = false;
if (resourceStream == null) {
fromUploadedMedia = true;
try {
MediaFileManager mmgr = WebloggerFactory.getWeblogger()
.getMediaFileManager();
Expand Down Expand Up @@ -159,8 +162,19 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
}

// set the content type based on whatever is in our web.xml mime defs
response.setContentType(this.context.getMimeType(resourceRequest
.getResourcePath()));
String resourceType = this.context.getMimeType(
resourceRequest.getResourcePath());
if (fromUploadedMedia) {
// Uploaded through the media library, so it is governed by the
// same policy as any other media response.
MediaTypePolicy.applyResponseHeaders(response, resourceType,
resourceRequest.getResourcePath());
} else {
// A theme resource: authored as part of the theme and served as
// the type the theme intends, but never re-typed by the browser.
response.setHeader("X-Content-Type-Options", "nosniff");
response.setContentType(resourceType);
}

try {
// ok, lets serve up the file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.apache.roller.weblogger.pojos.MediaFileDirectory;
import org.apache.roller.weblogger.util.RollerMessages;
import org.apache.roller.weblogger.util.RollerMessages.RollerMessage;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.util.Utilities;
import org.apache.struts2.interceptor.validation.SkipValidation;

Expand Down Expand Up @@ -173,23 +174,11 @@ public String save() {
mediaFile.setLength(this.uploadedFiles[i].length());
mediaFile.setInputStream(new FileInputStream(
this.uploadedFiles[i]));
mediaFile
.setContentType(this.uploadedFilesContentType[i]);

// in some cases Struts2 is not able to guess the content
// type correctly and assigns the default, which is
// octet-stream. So in cases where we see octet-stream
// we double check and see if we can guess the content
// type via the Java MIME type facilities.
mediaFile.setContentType(this.uploadedFilesContentType[i]);
if (mediaFile.getContentType() == null
|| mediaFile.getContentType().endsWith("/octet-stream")) {

String ctype = Utilities.getContentTypeFromFileName(mediaFile.getName());
if (null != ctype) {
mediaFile.setContentType(ctype);
}
}
// The type the browser put on the part describes what
// the sender meant to send. It is taken as a hint and
// the stored type is worked out from the file name.
mediaFile.setContentType(MediaTypePolicy.storedTypeFor(
mediaFile.getName(), this.uploadedFilesContentType[i]));

manager.createMediaFile(getActionWeblog(), mediaFile, errors);
WebloggerFactory.getWeblogger().flush();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.FileIOException;
import org.apache.roller.weblogger.business.MediaFileManager;
Expand Down Expand Up @@ -124,7 +125,10 @@ public String save() {

if (uploadedFile != null) {
mediaFile.setLength(this.uploadedFile.length());
mediaFile.setContentType(this.uploadedFileContentType);
// Replacing the body re-decides the type, on the same
// terms as the original upload.
mediaFile.setContentType(MediaTypePolicy.storedTypeFor(
mediaFile.getName(), this.uploadedFileContentType));
manager.updateMediaFile(getActionWeblog(), mediaFile,
new FileInputStream(this.uploadedFile));
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* 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.util;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import javax.servlet.http.HttpServletResponse;

/**
* Decides what type an uploaded file is stored as, and how it is served back.
*
* <p>A client uploading a file states a type, but the stored type is derived
* from the file name. The declared value is a hint only, consulted where the
* name yields nothing, and it cannot introduce a type the browser would
* execute.
*
* <p>Serving applies the second half. Only a short list of formats that
* browsers render passively are sent inline; everything else is sent as an
* attachment, and {@code nosniff} accompanies every response so browsers do
* not substitute their own type guess.
*/
public final class MediaTypePolicy {

private MediaTypePolicy() {
}

public static final String DEFAULT_TYPE = "application/octet-stream";

/**
* Formats browsers render without executing anything the file carries.
* SVG is deliberately absent: it is an XML document that can carry script.
*/
private static final Set<String> INLINE_TYPES = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(
"image/jpeg", "image/pjpeg", "image/png", "image/gif",
"image/bmp", "image/x-ms-bmp", "image/webp", "image/tiff",
"image/x-icon", "image/vnd.microsoft.icon",
"application/pdf")));

/** Families served inline whatever the subtype. */
private static final String[] INLINE_PREFIXES = {"audio/", "video/"};

/**
* Types a browser may execute, or that can carry something it will. These
* are never adopted from a client's declaration.
*/
private static final Set<String> ACTIVE_TYPES = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(
"text/html", "application/xhtml+xml", "application/xhtml",
"image/svg+xml", "text/xml", "application/xml",
"text/javascript", "application/javascript",
"application/ecmascript", "text/ecmascript",
"text/vbscript", "application/x-shockwave-flash",
"text/xsl", "application/xslt+xml")));

/**
* @param fileName the uploaded file's name
* @param declaredType the type the client said it was, may be null
* @return the type to store: derived from the name where that is
* conclusive, otherwise the declared type if it is not one a
* browser would act on, otherwise the generic binary type
*/
public static String storedTypeFor(String fileName, String declaredType) {
String derived = normalize(deriveFromName(fileName));
if (isConclusive(derived)) {
return derived;
}

String declared = normalize(declaredType);
if (isConclusive(declared) && !isActive(declared)) {
return declared;
}

return DEFAULT_TYPE;
}

/** @return true when browsers render this type without executing it */
public static boolean isInlineSafe(String contentType) {
String type = normalize(contentType);
if (type == null) {
return false;
}
if (INLINE_TYPES.contains(type)) {
return true;
}
for (String prefix : INLINE_PREFIXES) {
if (type.startsWith(prefix)) {
return true;
}
}
return false;
}

/** @return true when a browser may execute this type, or script inside it */
public static boolean isActive(String contentType) {
String type = normalize(contentType);
if (type == null) {
return false;
}
return ACTIVE_TYPES.contains(type) || type.endsWith("+xml");
}

/**
* Sets the type and the headers that govern how the response is treated.
* Anything outside the inline list is marked as an attachment.
*/
public static void applyResponseHeaders(HttpServletResponse response,
String contentType, String fileName) {
response.setHeader("X-Content-Type-Options", "nosniff");

String type = normalize(contentType);
if (type == null) {
type = DEFAULT_TYPE;
}

if (isInlineSafe(type)) {
response.setContentType(type);
return;
}

// Served as bytes to be saved rather than a document to be rendered.
response.setContentType(DEFAULT_TYPE);
response.setHeader("Content-Disposition",
"attachment; filename=\"" + headerSafe(fileName) + "\"");
}

private static String deriveFromName(String fileName) {
if (fileName == null || fileName.trim().isEmpty()) {
return null;
}
try {
return Utilities.getContentTypeFromFileName(fileName);
} catch (Exception undetermined) {
return null;
}
}

/** @return the bare type in lower case, without parameters such as charset */
private static String normalize(String contentType) {
if (contentType == null) {
return null;
}
String type = contentType.trim();
int semicolon = type.indexOf(';');
if (semicolon > -1) {
type = type.substring(0, semicolon).trim();
}
return type.isEmpty() ? null : type.toLowerCase(Locale.ENGLISH);
}

private static boolean isConclusive(String type) {
return type != null && !DEFAULT_TYPE.equals(type);
}

/**
* @return the name with the characters that would end the quoted string or
* start another header removed, since it is placed in one
*/
private static String headerSafe(String fileName) {
if (fileName == null || fileName.trim().isEmpty()) {
return "download";
}
String safe = fileName.replaceAll("[\\r\\n\"\\\\]", "");
return safe.trim().isEmpty() ? "download" : safe;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.FileIOException;
import org.apache.roller.weblogger.business.MediaFileManager;
Expand Down Expand Up @@ -136,7 +137,8 @@ public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
mf.setWeblog(website);
mf.setName(fileName);
mf.setOriginalPath(justPath);
mf.setContentType(contentType);
mf.setContentType(
MediaTypePolicy.storedTypeFor(fileName, contentType));
mf.setInputStream(fis);
mf.setLength(tempFile.length());

Expand Down Expand Up @@ -394,7 +396,8 @@ public void putMedia(AtomRequest areq) throws AtomException {

// Attempt to load file, to ensure it exists
MediaFile mf = fmgr.getMediaFileByPath(website, path);
mf.setContentType(contentType);
mf.setContentType(
MediaTypePolicy.storedTypeFor(mf.getName(), contentType));
mf.setInputStream(fis);
mf.setLength(tempFile.length());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.util.RollerConstants;
import org.apache.roller.weblogger.util.MediaTypePolicy;
import org.apache.roller.weblogger.business.MediaFileManager;
import org.apache.roller.weblogger.business.URLStrategy;
import org.apache.roller.weblogger.business.WeblogEntryManager;
Expand Down Expand Up @@ -381,7 +382,7 @@ public Object newMediaObject(String blogid, String userid, String password,
mf.setDirectory(root);
mf.setWeblog(website);
mf.setName(name);
mf.setContentType(type);
mf.setContentType(MediaTypePolicy.storedTypeFor(name, type));
mf.setInputStream(new ByteArrayInputStream(bits));
mf.setLength(bits.length);
String fileLink = mf.getPermalink();
Expand Down
Loading
Loading