/*
* Copyright 2013 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.parsepasses.contextautoesc;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Ordering;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.exprparse.SoyParsingContext;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.soytree.EscapingMode;
import com.google.template.soy.soytree.HtmlContext;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.PrintDirectiveNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import com.google.template.soy.soytree.defn.InjectedParam;
import com.google.template.soy.types.primitive.StringType;
import java.util.List;
/**
* Inserts attributes into templates to bless inline {@code <script>} and {@code <style>} elements
* and inline event handler and style attributes so that the browser can distinguish scripts
* specified by the template author from ones injected via XSS.
*
* <p>This class converts templates by adding {@code nonce="..."} to {@code <script>} and {@code
* <style>} elements, so
*
* <blockquote>
*
* {@code <script>...</script>}
*
* </blockquote>
*
* becomes
*
* <blockquote>
*
* {@code <script{if $ij.csp_nonce} nonce="{$ij.csp_nonce}"{/if}>...</script>}
*
* </blockquote>
*
* which authorize scripts in HTML pages that are governed by the <i>Content Security Policy</i>.
*
* <p>This class assumes that the value of {@code $ij.csp_nonce} will either be null or a valid <a
* href="//dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html#dfn-a-valid-nonce"
* >CSP-style "nonce"</a>, an unguessable string consisting of Latin Alpha-numeric characters, plus
* ({@code '+'}), and solidus ({@code '/'}).
*
* <blockquote>
*
* {@code nonce-value = 1*( ALPHA / DIGIT / "+" / "/" )}
*
* </blockquote>
*
* <h3>Dependencies</h3>
*
* <p>If inline event handlers or styles are used, then the page should also load {@code
* security.CspVerifier} which verifies event handler values.
*
* <h3>Caveats</h3>
*
* <p>This class does not add any {@code <meta http-equiv="content-security-policy" ...>} elements
* to the template. The application developer must specify the CSP policy headers and include the
* nonce there.
*
* <p>Nonces should be of sufficient length, and from a crypto-strong source of randomness. The
* stock <code>java.util.Random</code> is not strong enough, though a properly seeded <code>
* SecureRandom</code> is ok.
*
*/
public final class ContentSecurityPolicyPass {
private ContentSecurityPolicyPass() {
// Not instantiable.
}
/** The unprefixed name of the injected variable that holds the CSP nonce value for the page. */
public static final String CSP_NONCE_VARIABLE_NAME = "csp_nonce";
/** The name of the CSP nonce attribute, equals sign, and opening double quote. */
private static final String NONCE_ATTR_BEFORE_VALUE = " nonce=\"";
/** The closing double quote that appears after an attribute value. */
private static final String ATTR_AFTER_VALUE = "\"";
/**
* A variable definition for {@code $ij.csp_nonce}.
*
* <p>Since this pass implicitly blesses scripts that appear in the template text, authors should
* not explicitly mention {@code $id.csp_nonce} in their template signatures, so we do not look
* for a declared variable definition.
*/
private static final InjectedParam IMPLICIT_CSP_NONCE_DEFN =
new InjectedParam(CSP_NONCE_VARIABLE_NAME, StringType.getInstance());
// ---------------------------------------------------------------------------------------------
// Predicates used to identify HTML element and attribute boundaries in templates.
// ---------------------------------------------------------------------------------------------
/**
* True for any context that occurs within a {@code <script>} or {@code <style>} open tag. {@code
* [START]} and {@code [END]} mark ranges of positions for which this predicate is true. {@code
* <script[START] src=[END]foo[START]>[END]body()</script>}.
*/
private static final Predicate<? super Context> IN_SCRIPT_OR_STYLE_TAG_PREDICATE =
new Predicate<Context>() {
@Override
public boolean apply(Context c) {
return (
// In a script tag or style,
(c.elType == Context.ElementType.SCRIPT || c.elType == Context.ElementType.STYLE)
&& c.state == HtmlContext.HTML_TAG
// but not in an attribute
&& c.attrType == Context.AttributeType.NONE);
}
};
/**
* True between the end of a {@code <script>} or {@code <style>}tag and the start of its end tag.
* {@code [START]} and {@code [END]} mark ranges of positions for which this predicate is true.
* {@code <script src=foo]>[START]body()[END]</script>}.
*/
private static final Predicate<? super Context> IN_SCRIPT_OR_STYLE_BODY_PREDICATE =
new Predicate<Context>() {
@Override
public boolean apply(Context c) {
return (
// If we're not in an attribute,
c.attrType == Context.AttributeType.NONE
// but we're in JS or CSS, then we must be in a script or style body.
&& (c.state == HtmlContext.JS || c.state == HtmlContext.CSS));
}
};
/** True immediately before an HTML attribute value. */
public static final Predicate<? super Context> HTML_BEFORE_ATTRIBUTE_VALUE =
new Predicate<Context>() {
@Override
public boolean apply(Context c) {
return c.state == HtmlContext.HTML_BEFORE_ATTRIBUTE_VALUE;
}
};
// ---------------------------------------------------------------------------------------------
// Generators for Soy nodes that mark JS as safe to run.
// ---------------------------------------------------------------------------------------------
/** Generates Soy nodes to inject at a specific location in a raw text node. */
private abstract static class InjectedSoyGenerator {
/** The raw text node into which to inject nodes. */
final RawTextNode rawTextNode;
/** The offset into rawTextNode's text at which to inject the nodes. */
final int offset;
/**
* @param rawTextNode The raw text node into which to inject nodes.
* @param offset the offset into rawTextNode's text at which to inject the nodes.
*/
InjectedSoyGenerator(RawTextNode rawTextNode, int offset) {
Preconditions.checkElementIndex(offset, rawTextNode.getRawText().length(), "text offset");
this.rawTextNode = rawTextNode;
this.offset = offset;
}
/**
* Generates standalone Soy nodes to inject at {@link #offset} in {@link #rawTextNode} and adds
* them to out.
*
* @param idGenerator generates IDs for newly created nodes.
* @param out receives nodes to add in the order they should be added.
*/
abstract void addNodesToInject(
IdGenerator idGenerator, ImmutableList.Builder<? super SoyNode.StandaloneNode> out);
}
private static final class NonceAttrGenerator extends InjectedSoyGenerator {
NonceAttrGenerator(RawTextNode rawTextNode, int offset) {
super(rawTextNode, offset);
}
/** Adds `<code> nonce="{$ij.csp_nonce}"</code>`. */
@Override
void addNodesToInject(
IdGenerator idGenerator, ImmutableList.Builder<? super SoyNode.StandaloneNode> out) {
out.add(
new RawTextNode(
idGenerator.genId(), NONCE_ATTR_BEFORE_VALUE, rawTextNode.getSourceLocation()));
out.add(
makeInjectedCspNoncePrintNode(
rawTextNode.getSourceLocation(), idGenerator, EscapingMode.FILTER_CSP_NONCE_VALUE));
out.add(
new RawTextNode(idGenerator.genId(), ATTR_AFTER_VALUE, rawTextNode.getSourceLocation()));
}
}
/** A group of InjectedSoyGenerators with the same raw text node and offset. */
private static final class GroupOfInjectedSoyGenerator extends InjectedSoyGenerator {
final ImmutableList<InjectedSoyGenerator> members;
/** @param group InjectedSoyGenerator with the same raw text node and offset. */
GroupOfInjectedSoyGenerator(List<? extends InjectedSoyGenerator> group) {
super(group.get(0).rawTextNode, group.get(0).offset);
members = ImmutableList.copyOf(group);
for (InjectedSoyGenerator member : members) {
if (member.rawTextNode != rawTextNode || member.offset != offset) {
throw new IllegalArgumentException("Invalid group member");
}
}
}
/** delegates to each member in-order to add nodes to out. */
@Override
void addNodesToInject(
IdGenerator idGenerator, ImmutableList.Builder<? super SoyNode.StandaloneNode> out) {
for (InjectedSoyGenerator member : members) {
member.addNodesToInject(idGenerator, out);
}
}
}
// ---------------------------------------------------------------------------------------------
// Soy tree traversal that injects Soy nodes to mark JS in templates as safe to run.
// ---------------------------------------------------------------------------------------------
/**
* Add attributes to author-specified scripts and styles so that they will continue to run even
* though the browser's CSP policy blocks injected scripts and styles.
*/
public static void blessAuthorSpecifiedScripts(
Iterable<? extends SlicedRawTextNode> slicedRawTextNodes) {
// Given
// <script type="text/javascript">
// alert(1337)
// </script>
// we want to produce
// <script type="text/javascript"{if $ij.csp_nonce} nonce="{$ij.csp_nonce}"{/if}>
// alert(1337)
// </script>
// We need the nonce value to be unguessable which means not reliably reusing the same value
// from one page render to the next.
// We do this in several steps.
// 1. Identify the end of each <script> and <style> tag.
// <script type="text/javascript">alert(1337)</script>
// ^-- Can insert more attributes here
// We use the contexts from the contextual auto-escaper to identify the boundary between
// the tag that starts a script element and its body.
// 2. Walk backwards over ">" and "/>" to find a place where it is safe to insert atttributes.
// 3. Create an InjectedSoyGenerator instance that encapsulates the content to insert.
// <script type="text/javascript">alert(1337)</script>
// ^-- Remember this location.
// 4. Group InjectedSoyGenerators at the same location so that we could inject multiple chunks
// of content at the same slice offset.
// 5. Create a conditional check at each unique location, {if $ij.csp_nonce}...{/if}, so that we
// don't insert CSP attributes when the template is applied without a secret.
// 6. Create Soy nodes to fill out the {if}
// <script> -> <script{if $ij.csp_nonce} nonce="{$ij.csp_nonce}"{/if}>
ImmutableList.Builder<InjectedSoyGenerator> injectedSoyGenerators = ImmutableList.builder();
// We look for the end of attributes before the end of tags so that the stable sort we use to
// group generators leaves any at attribute ends before the ones at the end of a tag.
findNonceAttrLocations(slicedRawTextNodes, injectedSoyGenerators);
ImmutableListMultimap<RawTextNode, InjectedSoyGenerator> groupedInjectedAttrs =
sortAndGroup(injectedSoyGenerators.build());
generateAndInsertSoyNodesWrappedInIfNode(groupedInjectedAttrs);
}
/**
* Handles steps 3-5 by creating a NonceAttrGenerator for each location at the ^ in {@code <script
* foo=bar^>} immediately after the run of attributes in a script tag.
*/
private static void findNonceAttrLocations(
Iterable<? extends SlicedRawTextNode> slicedRawTextNodes,
ImmutableList.Builder<InjectedSoyGenerator> out) {
// Step 3: identify slices that end a <script> element so we can find a location where it it is
// safe to insert an attribute.
for (SlicedRawTextNode.RawTextSlice slice :
SlicedRawTextNode.find(
slicedRawTextNodes,
null,
IN_SCRIPT_OR_STYLE_TAG_PREDICATE,
IN_SCRIPT_OR_STYLE_BODY_PREDICATE)) {
String rawText = slice.getRawText();
int rawTextLen = rawText.length();
// Step 4: find a safe place to insert attributes.
if (rawText.charAt(rawTextLen - 1) != '>') {
throw new IllegalStateException("Invalid tag end: " + rawText);
}
int insertionPoint = rawTextLen - 1;
// We can't put an attribute in the middle of an XML-style "/>" tag terminator.
if (insertionPoint - 1 >= 0 && rawText.charAt(insertionPoint - 1) == '/') {
--insertionPoint;
}
// Step 5: create a generator for the CSP nonce attribute.
out.add(
new NonceAttrGenerator(
slice.slicedRawTextNode.getRawTextNode(), slice.getStartOffset() + insertionPoint));
}
}
private static final Ordering<InjectedSoyGenerator> BY_OFFSET =
new Ordering<InjectedSoyGenerator>() {
@Override
public int compare(InjectedSoyGenerator o1, InjectedSoyGenerator o2) {
return Integer.compare(o1.offset, o2.offset);
}
};
/**
* Handles step 6 by converting a list of InjectedSoyGenerators into an equivalent list where
* there is only one per text node and offset, and where the list is sorted by text node ID and
* offset.
*/
private static ImmutableListMultimap<RawTextNode, InjectedSoyGenerator> sortAndGroup(
List<InjectedSoyGenerator> ungrouped) {
// Sort by node ID & offset
ListMultimap<RawTextNode, InjectedSoyGenerator> byNode =
MultimapBuilder.hashKeys().arrayListValues().build();
for (InjectedSoyGenerator generator : ungrouped) {
byNode.put(generator.rawTextNode, generator);
}
// Walk over list grouping members with the same raw text node and offset.
ImmutableListMultimap.Builder<RawTextNode, InjectedSoyGenerator> groupedAndSorted =
ImmutableListMultimap.builder();
for (RawTextNode node : byNode.keySet()) {
List<InjectedSoyGenerator> group = BY_OFFSET.sortedCopy(byNode.get(node));
for (int i = 0, end; i < group.size(); i++) {
InjectedSoyGenerator firstGroupMember = group.get(i);
end = i + 1;
while (end < group.size() && group.get(end).offset == firstGroupMember.offset) {
++end;
}
// NOTE: currently it doesn't appear to be possible for there to be multiple injectors at
// the same offset, but we support it nonetheless.
InjectedSoyGenerator groupGenerator =
end == i + 1
? firstGroupMember
: new GroupOfInjectedSoyGenerator(group.subList(i, end));
groupedAndSorted.put(node, groupGenerator);
}
}
return groupedAndSorted.build();
}
/**
* Handles steps 7 and 8 by applying the generators to create Soy nodes and injects them at the
* location in the template specified by {@link InjectedSoyGenerator#rawTextNode} and {@link
* InjectedSoyGenerator#offset}, splitting and replacing text nodes as necessary.
*
* <p>{@link RawTextNode}'s text cannot be changed, so generators with the same {@link
* RawTextNode} cannot be applied separately. This method takes a list of generators, so it can
* apply them in a batch and avoid conflicts.
*
* @param groupedInjectedAttrs A sorted, grouped, list of generators.
*/
private static void generateAndInsertSoyNodesWrappedInIfNode(
ImmutableListMultimap<RawTextNode, InjectedSoyGenerator> groupedInjectedAttrs) {
for (RawTextNode rawTextNode : groupedInjectedAttrs.keySet()) {
String rawText = rawTextNode.getRawText();
ParentSoyNode<StandaloneNode> parent = rawTextNode.getParent();
IdGenerator idGenerator =
parent.getNearestAncestor(SoyFileSetNode.class).getNodeIdGenerator();
// Split rawTextNode on the offsets, and at each split, insert a nonce value.
int textStart = 0;
int childIndex = parent.getChildIndex(rawTextNode);
parent.removeChild(rawTextNode);
for (InjectedSoyGenerator generator : groupedInjectedAttrs.get(rawTextNode)) {
int offset = generator.offset;
if (offset != textStart) {
RawTextNode textBefore =
new RawTextNode(
idGenerator.genId(),
rawText.substring(textStart, offset),
rawTextNode.getSourceLocation());
parent.addChild(childIndex, textBefore);
++childIndex;
textStart = offset;
}
// Step 7: add an {if $ij.csp_nonce}...{/if} to prevent generation of CSP nonce when the
// template is applied without a secret.
IfNode ifNode = new IfNode(idGenerator.genId(), rawTextNode.getSourceLocation());
IfCondNode ifCondNode =
new IfCondNode(
idGenerator.genId(),
rawTextNode.getSourceLocation(),
"if",
makeReferenceToInjectedCspNonce(rawTextNode.getSourceLocation()));
parent.addChild(childIndex, ifNode);
++childIndex;
ifNode.addChild(ifCondNode);
// Step 8: inject Soy nodes into the {if}.
ImmutableList.Builder<SoyNode.StandaloneNode> newChildren = ImmutableList.builder();
generator.addNodesToInject(idGenerator, newChildren);
ifCondNode.addChildren(newChildren.build());
}
if (textStart != rawText.length()) {
RawTextNode textTail =
new RawTextNode(
idGenerator.genId(), rawText.substring(textStart), rawTextNode.getSourceLocation());
parent.addChild(childIndex, textTail);
}
}
}
// ---------------------------------------------------------------------------------------------
// Methods to programmatically create Soy commands and expressions.
// ---------------------------------------------------------------------------------------------
/** Builds the Soy expression {@code $ij.csp_nonce} with an appropriate type. */
private static ExprNode makeReferenceToInjectedCspNonce(SourceLocation location) {
return new VarRefNode(
CSP_NONCE_VARIABLE_NAME, location, true /*injected*/, IMPLICIT_CSP_NONCE_DEFN);
}
/** Builds the Soy command {@code {$ij.csp_nonce |escapeHtmlAttributeNospace}}. */
private static PrintNode makeInjectedCspNoncePrintNode(
SourceLocation location, IdGenerator idGenerator, EscapingMode escapeMode) {
PrintNode printNode =
new PrintNode.Builder(
idGenerator.genId(),
true, // Implicit. {$ij.csp_nonce} not {print $ij.csp_nonce}
location)
.exprRoot(new ExprRootNode(makeReferenceToInjectedCspNonce(location)))
.build(SoyParsingContext.exploding());
// Add an escaping directive to ensure that malicious csp_nonce values don't introduce XSSs
printNode.addChild(
new PrintDirectiveNode.Builder(idGenerator.genId(), escapeMode.directiveName, "", location)
.build(SoyParsingContext.exploding()));
return printNode;
}
}