package com.shapesecurity.salvation; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.shapesecurity.salvation.data.Base64Value; import com.shapesecurity.salvation.data.Location; import com.shapesecurity.salvation.data.Notice; import com.shapesecurity.salvation.data.Origin; import com.shapesecurity.salvation.data.Policy; import com.shapesecurity.salvation.data.SchemeHostPortTriple; import com.shapesecurity.salvation.data.URI; import com.shapesecurity.salvation.directiveValues.AncestorSource; import com.shapesecurity.salvation.directiveValues.HashSource; import com.shapesecurity.salvation.directiveValues.HostSource; import com.shapesecurity.salvation.directiveValues.KeywordSource; import com.shapesecurity.salvation.directiveValues.MediaType; import com.shapesecurity.salvation.directiveValues.NonceSource; import com.shapesecurity.salvation.directiveValues.None; import com.shapesecurity.salvation.directiveValues.RFC7230Token; import com.shapesecurity.salvation.directiveValues.ReportToValue; import com.shapesecurity.salvation.directiveValues.SchemeSource; import com.shapesecurity.salvation.directiveValues.SourceExpression; import com.shapesecurity.salvation.directives.BaseUriDirective; import com.shapesecurity.salvation.directives.BlockAllMixedContentDirective; import com.shapesecurity.salvation.directives.ChildSrcDirective; import com.shapesecurity.salvation.directives.ConnectSrcDirective; import com.shapesecurity.salvation.directives.DefaultSrcDirective; import com.shapesecurity.salvation.directives.Directive; import com.shapesecurity.salvation.directives.DirectiveValue; import com.shapesecurity.salvation.directives.FontSrcDirective; import com.shapesecurity.salvation.directives.FormActionDirective; import com.shapesecurity.salvation.directives.FrameAncestorsDirective; import com.shapesecurity.salvation.directives.FrameSrcDirective; import com.shapesecurity.salvation.directives.ImgSrcDirective; import com.shapesecurity.salvation.directives.ManifestSrcDirective; import com.shapesecurity.salvation.directives.MediaSrcDirective; import com.shapesecurity.salvation.directives.NavigateToDirective; import com.shapesecurity.salvation.directives.ObjectSrcDirective; import com.shapesecurity.salvation.directives.PluginTypesDirective; import com.shapesecurity.salvation.directives.PrefetchSrcDirective; import com.shapesecurity.salvation.directives.ReferrerDirective; import com.shapesecurity.salvation.directives.ReportToDirective; import com.shapesecurity.salvation.directives.ReportUriDirective; import com.shapesecurity.salvation.directives.RequireSriForDirective; import com.shapesecurity.salvation.directives.SandboxDirective; import com.shapesecurity.salvation.directives.ScriptSrcAttrDirective; import com.shapesecurity.salvation.directives.ScriptSrcDirective; import com.shapesecurity.salvation.directives.ScriptSrcElemDirective; import com.shapesecurity.salvation.directives.StyleSrcAttrDirective; import com.shapesecurity.salvation.directives.StyleSrcDirective; import com.shapesecurity.salvation.directives.StyleSrcElemDirective; import com.shapesecurity.salvation.directives.UpgradeInsecureRequestsDirective; import com.shapesecurity.salvation.directives.WorkerSrcDirective; import com.shapesecurity.salvation.tokens.DirectiveNameToken; import com.shapesecurity.salvation.tokens.DirectiveSeparatorToken; import com.shapesecurity.salvation.tokens.DirectiveValueToken; import com.shapesecurity.salvation.tokens.PolicySeparatorToken; import com.shapesecurity.salvation.tokens.SubDirectiveValueToken; import com.shapesecurity.salvation.tokens.Token; import com.shapesecurity.salvation.tokens.UnknownToken; public class Parser { private static final DirectiveParseException MISSING_DIRECTIVE_NAME = new DirectiveParseException("Missing directive-name"); private static final DirectiveParseException INVALID_DIRECTIVE_NAME = new DirectiveParseException("Invalid directive-name"); private static final DirectiveParseException INVALID_DIRECTIVE_VALUE = new DirectiveParseException("Invalid directive-value"); private static final DirectiveParseException INVALID_MEDIA_TYPE_LIST = new DirectiveParseException("Invalid media-type-list"); private static final DirectiveValueParseException INVALID_MEDIA_TYPE = new DirectiveValueParseException("Invalid media-type"); private static final DirectiveParseException INVALID_SOURCE_LIST = new DirectiveParseException("Invalid source-list"); private static final DirectiveValueParseException INVALID_SOURCE_EXPR = new DirectiveValueParseException("Invalid source-expression"); private static final DirectiveParseException INVALID_ANCESTOR_SOURCE_LIST = new DirectiveParseException("Invalid ancestor-source-list"); private static final DirectiveValueParseException INVALID_ANCESTOR_SOURCE = new DirectiveValueParseException("Invalid ancestor-source"); private static final DirectiveParseException INVALID_REFERRER_TOKEN = new DirectiveParseException("Invalid referrer token"); private static final DirectiveParseException INVALID_REPORT_TO_TOKEN = new DirectiveParseException("Invalid report-to token"); private static final DirectiveParseException INVALID_REQUIRE_SRI_FOR_TOKEN_LIST = new DirectiveParseException("Invalid require-sri-for token list"); private static final DirectiveValueParseException INVALID_REQUIRE_SRI_FOR_TOKEN = new DirectiveValueParseException("Invalid require-sri-for token"); private static final DirectiveParseException INVALID_SANDBOX_TOKEN_LIST = new DirectiveParseException("Invalid sandbox token list"); private static final DirectiveValueParseException INVALID_SANDBOX_TOKEN = new DirectiveValueParseException("Invalid sandbox token"); private static final DirectiveParseException INVALID_URI_REFERENCE_LIST = new DirectiveParseException("Invalid uri-reference list"); private static final DirectiveValueParseException INVALID_URI_REFERENCE = new DirectiveValueParseException("Invalid uri-reference"); private static final DirectiveParseException NON_EMPTY_VALUE_TOKEN_LIST = new DirectiveParseException("Non-empty directive-value list"); private static final String explanation = "Ensure that this pattern is only used for backwards compatibility with older CSP implementations and is not an oversight."; private static final String unsafeInlineWarningMessage = "The \"'unsafe-inline'\" keyword-source has no effect in source lists that contain hash-source or nonce-source in CSP2 and later. " + explanation; private static final String strictDynamicWarningMessage = "The host-source and scheme-source expressions, as well as the \"'unsafe-inline'\" and \"'self'\" keyword-sources have no effect in source lists that contain \"'strict-dynamic'\" in CSP3 and later. " + explanation; private static final String unsafeHashesWithoutHashWarningMessage = "The \"'unsafe-hashes'\" keyword-source has no effect in source lists that do not contain hash-source in CSP3 and later."; private enum SeenStates { SEEN_HASH, SEEN_HOST_OR_SCHEME_SOURCE, SEEN_NONE, SEEN_NONCE, SEEN_SELF, SEEN_STRICT_DYNAMIC, SEEN_UNSAFE_EVAL, SEEN_UNSAFE_INLINE, SEEN_UNSAFE_HASHES, SEEN_REPORT_SAMPLE, SEEN_UNSAFE_ALLOW_REDIRECTS } @Nonnull protected final Token[] tokens; @Nonnull private final Origin origin; protected int index = 0; @Nullable protected Collection<Notice> noticesOut; protected Parser(@Nonnull Token[] tokens, @Nonnull Origin origin, @Nullable Collection<Notice> noticesOut) { this.origin = origin; this.tokens = tokens; this.noticesOut = noticesOut; } @Nonnull public static Policy parse(@Nonnull String sourceText, @Nonnull Origin origin) { return new Parser(Tokeniser.tokenise(sourceText), origin, null).parsePolicyAndAssertEOF(); } @Nonnull public static Policy parse(@Nonnull String sourceText, @Nonnull String origin) { return new Parser(Tokeniser.tokenise(sourceText), URI.parse(origin), null).parsePolicyAndAssertEOF(); } @Nonnull public static Policy parse(@Nonnull String sourceText, @Nonnull Origin origin, @Nonnull Collection<Notice> warningsOut) { return new Parser(Tokeniser.tokenise(sourceText), origin, warningsOut).parsePolicyAndAssertEOF(); } @Nonnull public static Policy parse(@Nonnull String sourceText, @Nonnull String origin, @Nonnull Collection<Notice> warningsOut) { return new Parser(Tokeniser.tokenise(sourceText), URI.parse(origin), warningsOut).parsePolicyAndAssertEOF(); } @Nonnull public static List<Policy> parseMulti(@Nonnull String sourceText, @Nonnull Origin origin) { return new Parser(Tokeniser.tokenise(sourceText), origin, null).parsePolicyListAndAssertEOF(); } @Nonnull public static List<Policy> parseMulti(@Nonnull String sourceText, @Nonnull String origin) { return new Parser(Tokeniser.tokenise(sourceText), URI.parse(origin), null).parsePolicyListAndAssertEOF(); } @Nonnull public static List<Policy> parseMulti(@Nonnull String sourceText, @Nonnull Origin origin, @Nonnull Collection<Notice> warningsOut) { return new Parser(Tokeniser.tokenise(sourceText), origin, warningsOut).parsePolicyListAndAssertEOF(); } @Nonnull public static List<Policy> parseMulti(@Nonnull String sourceText, @Nonnull String origin, @Nonnull Collection<Notice> warningsOut) { return new Parser(Tokeniser.tokenise(sourceText), URI.parse(origin), warningsOut).parsePolicyListAndAssertEOF(); } @Nonnull protected Notice createNotice(@Nonnull Notice.Type type, @Nonnull String message) { return new Notice(type, message); } @Nonnull protected Notice createNotice(@Nullable Token token, @Nonnull Notice.Type type, @Nonnull String message) { return new Notice(type, message); } private void warn(@Nullable Token token, @Nonnull String message) { if (this.noticesOut != null) { this.noticesOut.add(this.createNotice(token, Notice.Type.WARNING, message)); } } private void error(@Nullable Token token, @Nonnull String message) { if (this.noticesOut != null) { this.noticesOut.add(this.createNotice(token, Notice.Type.ERROR, message)); } } private void info(@Nullable Token token, @Nonnull String message) { if (this.noticesOut != null) { this.noticesOut.add(this.createNotice(token, Notice.Type.INFO, message)); } } @Nonnull private Token advance() { return this.tokens[this.index++]; } protected boolean hasNext() { return this.index < this.tokens.length; } private boolean hasNext(@Nonnull Class<? extends Token> c) { return this.hasNext() && c.isAssignableFrom(this.tokens[this.index].getClass()); } private boolean eat(@Nonnull Class<? extends Token> c) { if (this.hasNext(c)) { this.advance(); return true; } return false; } @Nonnull protected Policy parsePolicy() { Policy policy = new Policy(this.origin); LinkedHashMap<Class<? extends Directive>, Directive<? extends DirectiveValue>> directives = new LinkedHashMap<>(); while (this.hasNext()) { if (this.hasNext(PolicySeparatorToken.class)) { break; } if (this.eat(DirectiveSeparatorToken.class)) { continue; } try { Directive<? extends DirectiveValue> directive = this.parseDirective(); // only add a directive if it doesn't exist; used for handling duplicate directives in CSP headers if (!directives.containsKey(directive.getClass())) { directives.put(directive.getClass(), directive); } else { this.warn(this.tokens[this.index - 2], "Policy contains more than one " + directive.name + " directive. All but the first instance will be ignored."); } } catch (DirectiveParseException ignored) { } } policy.addDirectives(directives.values()); return policy; } @Nonnull protected Policy parsePolicyAndAssertEOF() { Policy policy = this.parsePolicy(); if (this.hasNext()) { Token t = this.advance(); this.error(t, "Expecting end of policy but found \"" + t.value + "\"."); } return policy; } @Nonnull protected List<Policy> parsePolicyList() { List<Policy> policies = new ArrayList<>(); policies.add(this.parsePolicy()); while (this.hasNext(PolicySeparatorToken.class)) { while (this.eat(PolicySeparatorToken.class)) ; policies.add(this.parsePolicy()); } return policies; } @Nonnull protected List<Policy> parsePolicyListAndAssertEOF() { List<Policy> policies = this.parsePolicyList(); if (this.hasNext()) { Token t = this.advance(); this.error(t, "Expecting end of policy list but found \"" + t.value + "\"."); } return policies; } @Nonnull private Directive<?> parseDirective() throws DirectiveParseException { if (!this.hasNext(DirectiveNameToken.class)) { Token t = this.advance(); this.error(t, "Expecting directive-name but found \"" + t.value.split(" ", 2)[0] + "\"."); throw MISSING_DIRECTIVE_NAME; } Directive result; DirectiveNameToken token = (DirectiveNameToken) this.advance(); try { switch (token.subtype) { case BaseUri: result = new BaseUriDirective(this.parseSourceList()); break; case BlockAllMixedContent: warnFutureDirective(token); this.enforceMissingDirectiveValue(token); result = new BlockAllMixedContentDirective(); break; case ChildSrc: this.warn(token, "The child-src directive is deprecated as of CSP level 3. Authors who wish to regulate nested browsing contexts and workers SHOULD use the frame-src and worker-src directives, respectively."); result = new ChildSrcDirective(this.parseSourceList()); break; case ConnectSrc: result = new ConnectSrcDirective(this.parseSourceList()); break; case DefaultSrc: result = new DefaultSrcDirective(this.parseSourceList()); break; case FontSrc: result = new FontSrcDirective(this.parseSourceList()); break; case FormAction: result = new FormActionDirective(this.parseSourceList()); break; case FrameAncestors: result = new FrameAncestorsDirective(this.parseAncestorSourceList()); break; case ImgSrc: result = new ImgSrcDirective(this.parseSourceList()); break; case ManifestSrc: warnFutureDirective(token); result = new ManifestSrcDirective(this.parseSourceList()); break; case MediaSrc: result = new MediaSrcDirective(this.parseSourceList()); break; case NavigateTo: result = new NavigateToDirective(this.parseSourceList()); break; case ObjectSrc: result = new ObjectSrcDirective(this.parseSourceList()); break; case PluginTypes: Set<MediaType> mediaTypes = this.parseMediaTypeList(); if (mediaTypes.isEmpty()) { this.error(token, "The media-type-list must contain at least one media-type."); throw INVALID_MEDIA_TYPE_LIST; } else if (mediaTypes.stream().anyMatch(x -> x.type.equals("*") || x.subtype.equals("*"))) { this.warn(token, "Media types can only be matched literally. Make sure using `*` is not an oversight."); } result = new PluginTypesDirective(mediaTypes); break; case PrefetchSrc: result = new PrefetchSrcDirective(this.parseSourceList()); break; case Referrer: this.warn(token, "The referrer directive was an experimental directive that was proposed but never added to the CSP specification. Support for this directive will be removed. See Referrer Policy specification."); result = new ReferrerDirective(this.parseReferrerToken(token)); break; case ReportTo: result = new ReportToDirective(this.parseReportToToken(token)); break; case ReportUri: // TODO: bump to .warn once CSP3 becomes RC this.info(token, "A draft of the next version of CSP deprecates report-uri in favour of a new report-to directive."); Set<URI> uriList = this.parseUriList(); if (uriList.isEmpty()) { this.error(token, "The report-uri directive must contain at least one uri-reference."); throw INVALID_URI_REFERENCE_LIST; } result = new ReportUriDirective(uriList); break; case RequireSriFor: result = new RequireSriForDirective(this.parseRequireSriForTokenList(token)); break; case Sandbox: result = new SandboxDirective(this.parseSandboxTokenList()); break; case ScriptSrc: result = new ScriptSrcDirective(this.parseSourceList()); break; case ScriptSrcElem: result = new ScriptSrcElemDirective(this.parseSourceList()); break; case ScriptSrcAttr: result = new ScriptSrcAttrDirective(this.parseSourceList()); break; case StyleSrc: result = new StyleSrcDirective(this.parseSourceList()); break; case StyleSrcElem: result = new StyleSrcElemDirective(this.parseSourceList()); break; case StyleSrcAttr: result = new StyleSrcAttrDirective(this.parseSourceList()); break; case UpgradeInsecureRequests: warnFutureDirective(token); this.enforceMissingDirectiveValue(token); result = new UpgradeInsecureRequestsDirective(); break; case WorkerSrc: result = new WorkerSrcDirective(this.parseSourceList()); break; case Allow: this.error(token, "The allow directive has been replaced with default-src and is not in the CSP specification."); this.eat(DirectiveValueToken.class); throw INVALID_DIRECTIVE_NAME; case FrameSrc: result = new FrameSrcDirective(this.parseSourceList()); break; case Options: this.error(token, "The options directive has been replaced with 'unsafe-inline' and 'unsafe-eval' and is not in the CSP specification."); this.eat(DirectiveValueToken.class); throw INVALID_DIRECTIVE_NAME; case Unrecognised: default: this.error(token, "Unrecognised directive-name: \"" + token.value + "\"."); this.eat(DirectiveValueToken.class); throw INVALID_DIRECTIVE_NAME; } } finally { if (this.hasNext(UnknownToken.class)) { Token t = this.advance(); int cp = t.value.codePointAt(0); this.error(t, String.format( "Expecting directive-value but found U+%04X (%s). Non-ASCII and non-printable characters must be percent-encoded.", cp, new String(new int[]{cp}, 0, 1))); throw INVALID_DIRECTIVE_VALUE; } } return result; } private void warnFutureDirective(DirectiveNameToken token) { this.warn(token, "The " + token.value + " directive is an experimental directive that will be likely added to the CSP specification."); } private void enforceMissingDirectiveValue(@Nonnull Token directiveNameToken) throws DirectiveParseException { if (this.eat(DirectiveValueToken.class)) { this.error(directiveNameToken, "The " + directiveNameToken.value + " directive must not contain any value."); throw NON_EMPTY_VALUE_TOKEN_LIST; } } @Nonnull private Set<MediaType> parseMediaTypeList() throws DirectiveParseException { Set<MediaType> mediaTypes = new LinkedHashSet<>(); boolean parseException = false; while (this.hasNext(SubDirectiveValueToken.class)) { try { mediaTypes.add(this.parseMediaType()); } catch (DirectiveValueParseException e) { parseException = true; } } if (parseException) { throw INVALID_MEDIA_TYPE_LIST; } return mediaTypes; } @Nonnull private MediaType parseMediaType() throws DirectiveValueParseException { Token token = this.advance(); Matcher matcher = Constants.mediaTypePattern.matcher(token.value); if (matcher.find()) { return new MediaType(matcher.group("type"), matcher.group("subtype")); } this.error(token, "Expecting media-type but found \"" + token.value + "\"."); throw INVALID_MEDIA_TYPE; } @Nonnull private Set<SourceExpression> parseSourceList() throws DirectiveParseException { Set<SourceExpression> sourceExpressions = new LinkedHashSet<>(); boolean parseException = false; Set<SeenStates> seenStates = new HashSet<>(); while (this.hasNext(SubDirectiveValueToken.class)) { try { SourceExpression se = this.parseSourceExpression(seenStates, !sourceExpressions.isEmpty()); if (se == None.INSTANCE) { seenStates.add(SeenStates.SEEN_NONE); } else if (se == KeywordSource.UnsafeEval) { seenStates.add(SeenStates.SEEN_UNSAFE_EVAL); } else if (se == KeywordSource.Self) { seenStates.add(SeenStates.SEEN_SELF); } else if (se == KeywordSource.UnsafeInline) { seenStates.add(SeenStates.SEEN_UNSAFE_INLINE); } else if (se instanceof HashSource) { seenStates.add(SeenStates.SEEN_HASH); } else if (se instanceof NonceSource) { seenStates.add(SeenStates.SEEN_NONCE); } else if (se == KeywordSource.StrictDynamic) { seenStates.add(SeenStates.SEEN_STRICT_DYNAMIC); } else if (se instanceof HostSource || se instanceof SchemeSource) { seenStates.add(SeenStates.SEEN_HOST_OR_SCHEME_SOURCE); } else if (se == KeywordSource.UnsafeHashes) { seenStates.add(SeenStates.SEEN_UNSAFE_HASHES); } else if (se == KeywordSource.ReportSample) { seenStates.add(SeenStates.SEEN_REPORT_SAMPLE); } else if (se == KeywordSource.UnsafeAllowRedirects) { seenStates.add(SeenStates.SEEN_UNSAFE_ALLOW_REDIRECTS); } if (!sourceExpressions.add(se)) { this.warn(this.tokens[this.index - 1], "Source list contains duplicate source expression \"" + se.show() + "\". All but the first instance will be ignored."); } } catch (DirectiveValueParseException e) { parseException = true; } } if (seenStates.contains(SeenStates.SEEN_UNSAFE_HASHES) && !seenStates.contains(SeenStates.SEEN_HASH)) { this.warn(this.tokens[0], unsafeHashesWithoutHashWarningMessage); } if (parseException) { throw INVALID_SOURCE_LIST; } return sourceExpressions; } @Nonnull private SourceExpression parseSourceExpression(Set<SeenStates> seenStates, boolean seenSome) throws DirectiveValueParseException { Token token = this.advance(); if (seenStates.contains(SeenStates.SEEN_NONE) || seenSome && token.value.equalsIgnoreCase("'none'")) { this.error(token, "'none' must not be combined with any other source-expression."); throw INVALID_SOURCE_EXPR; } switch (token.value.toLowerCase()) { case "'none'": return None.INSTANCE; case "'self'": if (seenStates.contains(SeenStates.SEEN_STRICT_DYNAMIC)) { this.info(token, strictDynamicWarningMessage); } return KeywordSource.Self; case "'strict-dynamic'": if (seenStates.contains(SeenStates.SEEN_UNSAFE_INLINE) || seenStates.contains(SeenStates.SEEN_HOST_OR_SCHEME_SOURCE) || seenStates.contains(SeenStates.SEEN_SELF)) { this.info(token, strictDynamicWarningMessage); } return KeywordSource.StrictDynamic; case "'unsafe-inline'": if (seenStates.contains(SeenStates.SEEN_HASH) || seenStates.contains(SeenStates.SEEN_NONCE)) { this.info(token, unsafeInlineWarningMessage); } if (seenStates.contains(SeenStates.SEEN_STRICT_DYNAMIC)) { this.info(token, strictDynamicWarningMessage); } return KeywordSource.UnsafeInline; case "'unsafe-eval'": return KeywordSource.UnsafeEval; case "'unsafe-redirect'": this.warn(token, "'unsafe-redirect' has been removed from CSP as of version 2.0."); return KeywordSource.UnsafeRedirect; case "'unsafe-hashes'": return KeywordSource.UnsafeHashes; case "'report-sample'": return KeywordSource.ReportSample; case "'unsafe-allow-redirects'": return KeywordSource.UnsafeAllowRedirects; default: checkForUnquotedKeyword(token); if (token.value.startsWith("'nonce-")) { String nonce = token.value.substring(7, token.value.length() - 1); NonceSource nonceSource = new NonceSource(nonce); nonceSource.validationErrors().forEach(str -> this.warn(token, str)); if (seenStates.contains(SeenStates.SEEN_UNSAFE_INLINE)) { this.info(token, unsafeInlineWarningMessage); } return nonceSource; } else if (token.value.toLowerCase().startsWith("'sha")) { HashSource.HashAlgorithm algorithm; switch (token.value.substring(4, 7)) { case "256": algorithm = HashSource.HashAlgorithm.SHA256; break; case "384": algorithm = HashSource.HashAlgorithm.SHA384; break; case "512": algorithm = HashSource.HashAlgorithm.SHA512; break; default: this.error(token, "Unrecognised hash algorithm: \"" + token.value.substring(1, 7) + "\"."); throw INVALID_SOURCE_EXPR; } String value = token.value.substring(8, token.value.length() - 1); // convert url-safe base64 to RFC4648 base64 String safeValue = value.replace('-', '+').replace('_', '/'); Base64Value base64Value; try { base64Value = new Base64Value(safeValue); } catch (IllegalArgumentException e) { this.error(token, e.getMessage()); throw INVALID_SOURCE_EXPR; } // warn if value is not RFC4648 if (value.contains("-") || value.contains("_")) { this.warn(token, "Invalid base64-value (characters are not in the base64-value grammar). Consider using RFC4648 compliant base64 encoding implementation."); } HashSource hashSource = new HashSource(algorithm, base64Value); try { hashSource.validationErrors(); } catch (IllegalArgumentException e) { this.error(token, e.getMessage()); throw INVALID_SOURCE_EXPR; } if (seenStates.contains(SeenStates.SEEN_UNSAFE_INLINE)) { this.info(token, unsafeInlineWarningMessage); } return hashSource; } else if (token.value.matches("^" + Constants.schemePart + ":$")) { if (seenStates.contains(SeenStates.SEEN_STRICT_DYNAMIC)) { this.info(token, strictDynamicWarningMessage); } return new SchemeSource(token.value.substring(0, token.value.length() - 1)); } else if (token.value.equalsIgnoreCase("'unsafe-hashed-attributes'")) { this.warn(token, "The CSP specification renamed 'unsafe-hashed-attributes' to 'unsafe-hashes' (June 2018)."); } else { Matcher matcher = Constants.hostSourcePattern.matcher(token.value); if (matcher.find()) { String scheme = matcher.group("scheme"); if (scheme != null) { scheme = scheme.substring(0, scheme.length() - 3); } String portString = matcher.group("port"); int port; if (portString == null) { port = scheme == null ? Constants.EMPTY_PORT : SchemeHostPortTriple.defaultPortForProtocol(scheme); } else { port = portString.equals(":*") ? Constants.WILDCARD_PORT : Integer.parseInt(portString.substring(1)); } if (seenStates.contains(SeenStates.SEEN_STRICT_DYNAMIC)) { this.info(token, strictDynamicWarningMessage); } String host = matcher.group("host"); String path = matcher.group("path"); return new HostSource(scheme, host, port, path); } } } this.error(token, "Expecting source-expression but found \"" + token.value + "\"."); throw INVALID_SOURCE_EXPR; } @Nonnull private Set<AncestorSource> parseAncestorSourceList() throws DirectiveParseException { Set<AncestorSource> ancestorSources = new LinkedHashSet<>(); boolean parseException = false; boolean seenNone = false; while (this.hasNext(SubDirectiveValueToken.class)) { try { AncestorSource ancestorSource = this.parseAncestorSource(seenNone, !ancestorSources.isEmpty()); if (ancestorSource == None.INSTANCE) { seenNone = true; } ancestorSources.add(ancestorSource); } catch (DirectiveValueParseException e) { parseException = true; } } if (parseException) { throw INVALID_ANCESTOR_SOURCE_LIST; } return ancestorSources; } @Nonnull private AncestorSource parseAncestorSource(boolean seenNone, boolean seenSome) throws DirectiveValueParseException { Token token = this.advance(); if (seenNone || seenSome && token.value.equalsIgnoreCase("'none'")) { this.error(token, "'none' must not be combined with any other ancestor-source."); throw INVALID_ANCESTOR_SOURCE; } if (token.value.equalsIgnoreCase("'none'")) { return None.INSTANCE; } if (token.value.equalsIgnoreCase("'self'")) { return KeywordSource.Self; } checkForUnquotedKeyword(token); if (token.value.matches("^" + Constants.schemePart + ":$")) { return new SchemeSource(token.value.substring(0, token.value.length() - 1)); } else { Matcher matcher = Constants.hostSourcePattern.matcher(token.value); if (matcher.find()) { String scheme = matcher.group("scheme"); if (scheme != null) { scheme = scheme.substring(0, scheme.length() - 3); } String portString = matcher.group("port"); int port; if (portString == null) { port = scheme == null ? Constants.EMPTY_PORT : SchemeHostPortTriple.defaultPortForProtocol(scheme); } else { port = portString.equals(":*") ? Constants.WILDCARD_PORT : Integer.parseInt(portString.substring(1)); } String host = matcher.group("host"); String path = matcher.group("path"); return new HostSource(scheme, host, port, path); } } this.error(token, "Expecting ancestor-source but found \"" + token.value + "\"."); throw INVALID_ANCESTOR_SOURCE; } private void checkForUnquotedKeyword(@Nonnull Token token) { if (Constants.unquotedKeywordPattern.matcher(token.value).find()) { this.warn(token, "This host name is unusual, and likely meant to be a keyword that is missing the required quotes: \'" + token.value + "\'."); } } @Nonnull private RFC7230Token parseReferrerToken(@Nonnull Token directiveNameToken) throws DirectiveParseException { if (this.hasNext(DirectiveValueToken.class)) { Token token = this.advance(); Matcher matcher = Constants.referrerTokenPattern.matcher(Tokeniser.trimRHSWS(token.value)); if (matcher.find()) { return new RFC7230Token(token.value); } this.error(token, "Expecting referrer directive value but found \"" + token.value + "\"."); } else { this.error(directiveNameToken, "The referrer directive must contain exactly one referrer directive value."); throw INVALID_DIRECTIVE_VALUE; } throw INVALID_REFERRER_TOKEN; } @Nonnull private ReportToValue parseReportToToken(@Nonnull Token directiveNameToken) throws DirectiveParseException { if (this.hasNext(DirectiveValueToken.class)) { Token token = this.advance(); Matcher matcher = Constants.rfc7230TokenPattern.matcher(Tokeniser.trimRHSWS(token.value)); if (matcher.find()) { return new ReportToValue(token.value); } this.error(token, "Expecting RFC 7230 token but found \"" + token.value + "\"."); } else { this.error(directiveNameToken, "The report-to directive must contain exactly one RFC 7230 token."); } throw INVALID_REPORT_TO_TOKEN; } @Nonnull private Set<RFC7230Token> parseRequireSriForTokenList(@Nonnull Token directiveNameToken) throws DirectiveParseException { Set<RFC7230Token> requireSriForTokens = new LinkedHashSet<>(); boolean parseException = false; while (this.hasNext(SubDirectiveValueToken.class)) { try { RFC7230Token rsfToken = this.parseRequireSriForToken(); if (!requireSriForTokens.add(rsfToken)) { this.warn(directiveNameToken, "The require-sri-for directive contains duplicate token: \"" + rsfToken.show() + "\"."); } } catch (DirectiveValueParseException e) { parseException = true; } } if (parseException) { throw INVALID_REQUIRE_SRI_FOR_TOKEN_LIST; } if (requireSriForTokens.isEmpty()) { this.warn(directiveNameToken, "Empty require-sri-for directive has no effect."); } return requireSriForTokens; } @Nonnull private RFC7230Token parseRequireSriForToken() throws DirectiveValueParseException { Token token = this.advance(); Matcher matcher = Constants.requireSriForEnumeratedTokenPattern.matcher(token.value); if (matcher.find()) { return new RFC7230Token(token.value.toLowerCase()); } else { this.warn(token, "The require-sri-for directive should contain only \"script\", \"style\" tokens."); matcher = Constants.rfc7230TokenPattern.matcher(token.value); if (matcher.find()) { return new RFC7230Token(token.value); } } this.error(token, "Expecting RFC 7230 token but found \"" + token.value + "\"."); throw INVALID_REQUIRE_SRI_FOR_TOKEN; } @Nonnull private Set<RFC7230Token> parseSandboxTokenList() throws DirectiveParseException { Set<RFC7230Token> sandboxTokens = new LinkedHashSet<>(); boolean parseException = false; while (this.hasNext(SubDirectiveValueToken.class)) { try { sandboxTokens.add(this.parseSandboxToken()); } catch (DirectiveValueParseException e) { parseException = true; } } if (parseException) { throw INVALID_SANDBOX_TOKEN_LIST; } return sandboxTokens; } @Nonnull private RFC7230Token parseSandboxToken() throws DirectiveValueParseException { Token token = this.advance(); Matcher matcher = Constants.sandboxEnumeratedTokenPattern.matcher(token.value); if (matcher.find()) { return new RFC7230Token(token.value); } else { this.warn(token, "The sandbox directive should contain only allow-forms, allow-modals, " + "allow-pointer-lock, allow-popups, allow-popups-to-escape-sandbox, " + "allow-same-origin, allow-scripts, or allow-top-navigation."); matcher = Constants.rfc7230TokenPattern.matcher(token.value); if (matcher.find()) { return new RFC7230Token(token.value); } } this.error(token, "Expecting RFC 7230 token but found \"" + token.value + "\"."); throw INVALID_SANDBOX_TOKEN; } @Nonnull private Set<URI> parseUriList() throws DirectiveParseException { Set<URI> uriList = new LinkedHashSet<>(); boolean parseException = false; while (this.hasNext(SubDirectiveValueToken.class)) { try { uriList.add(this.parseUri()); } catch (DirectiveValueParseException e) { parseException = true; } } if (parseException) { throw INVALID_URI_REFERENCE_LIST; } return uriList; } @Nonnull private URI parseUri() throws DirectiveValueParseException { Token token = this.advance(); try { return URI.parseWithOrigin(this.origin, token.value); } catch (IllegalArgumentException ignored) { this.error(token, "Expecting uri-reference but found \"" + token.value + "\"."); throw INVALID_URI_REFERENCE; } } private static class DirectiveParseException extends Exception { @Nullable Location startLocation; @Nullable Location endLocation; private DirectiveParseException(@Nonnull String message) { super(message); } @Nonnull @Override public String getMessage() { if (startLocation == null) { return super.getMessage(); } return startLocation.show() + ": " + super.getMessage(); } } protected static class DirectiveValueParseException extends Exception { @Nullable Location startLocation; @Nullable Location endLocation; private DirectiveValueParseException(@Nonnull String message) { super(message); } @Nonnull @Override public String getMessage() { if (startLocation == null) { return super.getMessage(); } return startLocation.show() + ": " + super.getMessage(); } } }