/* * Copyright 2012 Google Inc. * * Licensed 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. */ package com.google.template.soy.data; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.html.types.SafeHtml; import com.google.common.html.types.SafeHtmlProto; import com.google.common.html.types.SafeHtmls; import com.google.common.html.types.SafeScript; import com.google.common.html.types.SafeScriptProto; import com.google.common.html.types.SafeScripts; import com.google.common.html.types.SafeStyle; import com.google.common.html.types.SafeStyleProto; import com.google.common.html.types.SafeStyleSheet; import com.google.common.html.types.SafeStyleSheetProto; import com.google.common.html.types.SafeStyleSheets; import com.google.common.html.types.SafeStyles; import com.google.common.html.types.SafeUrl; import com.google.common.html.types.SafeUrlProto; import com.google.common.html.types.SafeUrls; import com.google.common.html.types.TrustedResourceUrl; import com.google.common.html.types.TrustedResourceUrlProto; import com.google.common.html.types.TrustedResourceUrls; import com.google.common.io.Resources; import com.google.errorprone.annotations.CompileTimeConstant; import com.google.template.soy.data.SanitizedContent.ContentKind; import java.io.IOException; import java.nio.charset.Charset; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; /** * Creation utilities for SanitizedContent objects for common use cases. * * <p>This should contain utilities that have extremely broad application. More specific utilities * should reside with the specific project. * * <p>All utilities here should be extremely difficult to abuse in a way that could create * attacker-controlled SanitizedContent objects. Java's type system is a great tool to achieve this. * */ @ParametersAreNonnullByDefault public final class SanitizedContents { /** Extensions for static resources that we allow to be treated as safe HTML. */ private static final ImmutableSet<String> SAFE_HTML_FILE_EXTENSIONS = ImmutableSet.of("html", "svg"); /** No constructor. */ private SanitizedContents() {} /** Creates an empty string constant. */ public static SanitizedContent emptyString(ContentKind kind) { return SanitizedContent.create("", kind, Dir.NEUTRAL); // Empty string is neutral. } /** * Creates a SanitizedContent object of kind TEXT of a given direction (null if unknown). * * <p>This is useful when stubbing out a function that needs to create a SanitizedContent object. */ public static SanitizedContent unsanitizedText(String text, @Nullable Dir dir) { return SanitizedContent.create(text, ContentKind.TEXT, dir); } /** * Creates a SanitizedContent object of kind TEXT and unknown direction. * * <p>This is useful when stubbing out a function that needs to create a SanitizedContent object. */ public static SanitizedContent unsanitizedText(String text) { return unsanitizedText(text, null); } /** * Concatenate the contents of multiple {@link SanitizedContent} objects of kind HTML. * * @param contents The HTML content to combine. */ public static SanitizedContent concatHtml(SanitizedContent... contents) { for (SanitizedContent content : contents) { checkArgument(content.getContentKind() == ContentKind.HTML, "Can only concat HTML"); } StringBuilder combined = new StringBuilder(); Dir dir = Dir.NEUTRAL; // Empty string is neutral. for (SanitizedContent content : contents) { combined.append(content.getContent()); if (dir == Dir.NEUTRAL) { // neutral + x -> x dir = content.getContentDirection(); } else if (content.getContentDirection() == dir || content.getContentDirection() == Dir.NEUTRAL) { // x + x|neutral -> x, so leave dir unchanged. } else { // LTR|unknown + RTL|unknown -> unknown // RTL|unknown + LTR|unknown -> unknown dir = null; } } return SanitizedContent.create(combined.toString(), ContentKind.HTML, dir); } /** * Loads assumed-safe content from a Java resource. * * <p>This performs ZERO VALIDATION of the data, and takes you on your word that the input is * valid. We assume that resources should be safe because they are part of the binary, and * therefore not attacker controlled, unless the source code is compromised (in which there's * nothing we can do). * * @param contextClass Class relative to which to load the resource. * @param resourceName The name of the resource, relative to the context class. * @param charset The character set to use, usually Charsets.UTF_8. * @param kind The content kind of the resource. */ public static SanitizedContent fromResource( Class<?> contextClass, String resourceName, Charset charset, ContentKind kind) throws IOException { pretendValidateResource(resourceName, kind); return SanitizedContent.create( Resources.toString(Resources.getResource(contextClass, resourceName), charset), kind, // Text resources are usually localized, so one might think that the locale direction should // be assumed for them. We do not do that because: // - We do not know the locale direction here. // - Some messages do not get translated. // - This method currently can't be used for text resources (see pretendValidateResource()). getDefaultDir(kind)); } /** * Loads assumed-safe content from a Java resource. * * <p>This performs ZERO VALIDATION of the data, and takes you on your word that the input is * valid. We assume that resources should be safe because they are part of the binary, and * therefore not attacker controlled, unless the source code is compromised (in which there's * nothing we can do). * * @param resourceName The name of the resource to be found using {@linkplain * Thread#getContextClassLoader() context class loader}. * @param charset The character set to use, usually Charsets.UTF_8. * @param kind The content kind of the resource. */ public static SanitizedContent fromResource( String resourceName, Charset charset, ContentKind kind) throws IOException { pretendValidateResource(resourceName, kind); return SanitizedContent.create( Resources.toString(Resources.getResource(resourceName), charset), kind, // Text resources are usually localized, so one might think that the locale direction should // be assumed for them. We do not do that because: // - We do not know the locale direction here. // - Some messages do not get translated. // - This method currently can't be used for text resources (see pretendValidateResource()). getDefaultDir(kind)); } /** * Wraps an assumed-safe URI constant. * * <p>This only accepts compile-time constants, based on the assumption that URLs that are * controlled by the application (and not user input) are considered safe. */ public static SanitizedContent constantUri(@CompileTimeConstant final String constant) { return fromConstant(constant, ContentKind.URI, Dir.LTR); } /** * Wraps an assumed-safe constant string that specifies a safe, balanced, document fragment. * * <p>This only accepts compile-time constants, based on the assumption that HTML snippets that * are controlled by the application (and not user input) are considered safe. */ public static SanitizedContent constantHtml(@CompileTimeConstant final String constant) { return fromConstant(constant, ContentKind.HTML, null); } /** Wraps an assumed-safe constant string. */ private static SanitizedContent fromConstant( String constant, ContentKind kind, @Nullable Dir dir) { // Extra runtime check in case the compile-time check doesn't work. Preconditions.checkArgument( constant.intern().equals(constant), "The provided argument does not look like a compile-time constant."); return SanitizedContent.create(constant, kind, dir); } /** Converts a {@link SafeHtml} into a Soy {@link SanitizedContent} of kind HTML. */ public static SanitizedContent fromSafeHtml(SafeHtml html) { return SanitizedContent.create(html.getSafeHtmlString(), ContentKind.HTML, null); } /** Converts a {@link SafeHtmlProto} into a Soy {@link SanitizedContent} of kind HTML. */ public static SanitizedContent fromSafeHtmlProto(SafeHtmlProto html) { return SanitizedContent.create( SafeHtmls.fromProto(html).getSafeHtmlString(), ContentKind.HTML, null); } /** Converts a {@link SafeScript} into a Soy {@link SanitizedContent} of kind JS. */ public static SanitizedContent fromSafeScript(SafeScript script) { return SanitizedContent.create(script.getSafeScriptString(), ContentKind.JS, null); } /** Converts a {@link SafeScriptProto} into a Soy {@link SanitizedContent} of kind JS. */ public static SanitizedContent fromSafeScriptProto(SafeScriptProto script) { return SanitizedContent.create( SafeScripts.fromProto(script).getSafeScriptString(), ContentKind.JS, null); } /** Converts a {@link SafeStyle} into a Soy {@link SanitizedContent} of kind CSS. */ public static SanitizedContent fromSafeStyle(SafeStyle style) { return SanitizedContent.create(style.getSafeStyleString(), ContentKind.CSS, null); } /** Converts a {@link SafeStyleProto} into a Soy {@link SanitizedContent} of kind CSS. */ public static SanitizedContent fromSafeStyleProto(SafeStyleProto style) { return SanitizedContent.create( SafeStyles.fromProto(style).getSafeStyleString(), ContentKind.CSS, null); } /** Converts a {@link SafeStyleSheet} into a Soy {@link SanitizedContent} of kind CSS. */ public static SanitizedContent fromSafeStyleSheet(SafeStyleSheet styleSheet) { return SanitizedContent.create(styleSheet.getSafeStyleSheetString(), ContentKind.CSS, null); } /** Converts a {@link SafeStyleSheetProto} into a Soy {@link SanitizedContent} of kind CSS. */ public static SanitizedContent fromSafeStyleSheetProto(SafeStyleSheetProto styleSheet) { return SanitizedContent.create( SafeStyleSheets.fromProto(styleSheet).getSafeStyleSheetString(), ContentKind.CSS, null); } /** Converts a {@link SafeUrl} into a Soy {@link SanitizedContent} of kind URI. */ public static SanitizedContent fromSafeUrl(SafeUrl url) { return SanitizedContent.create(url.getSafeUrlString(), ContentKind.URI, Dir.LTR); } /** Converts a {@link SafeUrlProto} into a Soy {@link SanitizedContent} of kind URI. */ public static SanitizedContent fromSafeUrlProto(SafeUrlProto url) { return SanitizedContent.create( SafeUrls.fromProto(url).getSafeUrlString(), ContentKind.URI, Dir.LTR); } /** * Converts a {@link TrustedResourceUrl} into a Soy {@link SanitizedContent} of kind * TRUSTED_RESOURCE_URI. */ public static SanitizedContent fromTrustedResourceUrl(TrustedResourceUrl url) { return SanitizedContent.create( url.getTrustedResourceUrlString(), ContentKind.TRUSTED_RESOURCE_URI, Dir.LTR); } /** * Converts a {@link TrustedResourceUrlProto} into a Soy {@link SanitizedContent} of kind * TRUSTED_RESOURCE_URI. */ public static SanitizedContent fromTrustedResourceUrlProto(TrustedResourceUrlProto url) { return SanitizedContent.create( TrustedResourceUrls.fromProto(url).getTrustedResourceUrlString(), ContentKind.TRUSTED_RESOURCE_URI, Dir.LTR); } /** * Very basic but strict validation that the resource's extension matches the content kind. * * <p>In practice, this may be unnecessary, but it's always good to start out strict. This list * can either be expanded as needed, or removed if too onerous. */ @VisibleForTesting static void pretendValidateResource(String resourceName, ContentKind kind) { int index = resourceName.lastIndexOf('.'); Preconditions.checkArgument( index >= 0, "Currently, we only validate resources with explicit extensions."); String fileExtension = resourceName.substring(index + 1).toLowerCase(); switch (kind) { case JS: Preconditions.checkArgument(fileExtension.equals("js")); break; case HTML: Preconditions.checkArgument(SAFE_HTML_FILE_EXTENSIONS.contains(fileExtension)); break; case CSS: Preconditions.checkArgument(fileExtension.equals("css")); break; default: throw new IllegalArgumentException("Don't know how to validate resources of kind " + kind); } } /* * Returns the default direction per content kind: LTR for JS, URI, ATTRIBUTES, and CSS content, * and otherwise unknown. */ static Dir getDefaultDir(ContentKind kind) { switch (kind) { case JS: case URI: case ATTRIBUTES: case CSS: case TRUSTED_RESOURCE_URI: return Dir.LTR; default: return null; } } }