Repository: shapesecurity/salvation
Pull request #189 (2018-01-19 00:25:24)

Support `prefetch-src` directive
Fixes https://github.com/shapesecurity/salvation/issues/188


Patch:
diff --git a/src/main/java/com/shapesecurity/salvation/Parser.java b/src/main/java/com/shapesecurity/salvation/Parser.java
index d036143..accd475 100644
--- a/src/main/java/com/shapesecurity/salvation/Parser.java
+++ b/src/main/java/com/shapesecurity/salvation/Parser.java
@@ -1,14 +1,67 @@
 package com.shapesecurity.salvation;
 
-import com.shapesecurity.salvation.data.*;
-import com.shapesecurity.salvation.directiveValues.*;
-import com.shapesecurity.salvation.directives.*;
-import com.shapesecurity.salvation.tokens.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+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 java.util.*;
-import java.util.regex.Matcher;
+
+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.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.ScriptSrcDirective;
+import com.shapesecurity.salvation.directives.StyleSrcDirective;
+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 {
 
@@ -257,6 +310,9 @@ public class Parser {
                     }
                     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.");
diff --git a/src/main/java/com/shapesecurity/salvation/Tokeniser.java b/src/main/java/com/shapesecurity/salvation/Tokeniser.java
index 8990a21..6663bbd 100644
--- a/src/main/java/com/shapesecurity/salvation/Tokeniser.java
+++ b/src/main/java/com/shapesecurity/salvation/Tokeniser.java
@@ -1,16 +1,23 @@
 package com.shapesecurity.salvation;
 
-import com.shapesecurity.salvation.data.Location;
-import com.shapesecurity.salvation.tokens.*;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.shapesecurity.salvation.data.Location;
+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 Tokeniser {
     private static final Pattern WSP = Pattern.compile("[ \t]+");
     private static final Pattern NotWSP = Pattern.compile("[^ \t]+");
@@ -130,6 +137,7 @@ public class Tokeniser {
                     case ManifestSrc:
                     case MediaSrc:
                     case ObjectSrc:
+                    case PrefetchSrc:
                     case ScriptSrc:
                     case StyleSrc:
                     case WorkerSrc:
diff --git a/src/main/java/com/shapesecurity/salvation/data/Policy.java b/src/main/java/com/shapesecurity/salvation/data/Policy.java
index 04c014a..d5d8751 100644
--- a/src/main/java/com/shapesecurity/salvation/data/Policy.java
+++ b/src/main/java/com/shapesecurity/salvation/data/Policy.java
@@ -40,6 +40,7 @@ import com.shapesecurity.salvation.directives.ManifestSrcDirective;
 import com.shapesecurity.salvation.directives.MediaSrcDirective;
 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;
@@ -149,6 +150,9 @@ public class Policy implements Show {
             if (!this.directives.containsKey(ManifestSrcDirective.class)) {
                 this.unionDirectivePrivate(new ManifestSrcDirective(defaultSources));
             }
+            if (!this.directives.containsKey(PrefetchSrcDirective.class)) {
+                this.unionDirectivePrivate(new PrefetchSrcDirective(defaultSources));
+            }
         }
 
         // expand child-src
@@ -517,6 +521,22 @@ public class Policy implements Show {
         return imgSrcDirective.matchesSource(this.origin, source);
     }
 
+    public boolean allowsPrefetchFromSource(@Nonnull URI source) {
+        PrefetchSrcDirective prefetchSrcDirective = this.getDirectiveByType(PrefetchSrcDirective.class);
+        if (prefetchSrcDirective == null) {
+            return this.defaultsAllowSource(source);
+        }
+        return prefetchSrcDirective.matchesSource(this.origin, source);
+    }
+
+    public boolean allowsPrefetchFromSource(@Nonnull GUID source) {
+        PrefetchSrcDirective PrefetchSrcDirective = this.getDirectiveByType(PrefetchSrcDirective.class);
+        if (PrefetchSrcDirective == null) {
+            return this.defaultsAllowSource(source);
+        }
+        return PrefetchSrcDirective.matchesSource(this.origin, source);
+    }
+
     public boolean allowsScriptFromSource(@Nonnull URI source) {
         ScriptSrcDirective scriptSrcDirective = this.getDirectiveByType(ScriptSrcDirective.class);
         if (scriptSrcDirective == null) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/Directive.java b/src/main/java/com/shapesecurity/salvation/directives/Directive.java
index 6ff1af5..df87773 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/Directive.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/Directive.java
@@ -1,16 +1,22 @@
 package com.shapesecurity.salvation.directives;
 
-import com.shapesecurity.salvation.directiveValues.HostSource;
-import com.shapesecurity.salvation.directiveValues.None;
-import com.shapesecurity.salvation.interfaces.Show;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.shapesecurity.salvation.directiveValues.HostSource;
+import com.shapesecurity.salvation.directiveValues.None;
+import com.shapesecurity.salvation.interfaces.Show;
+
 public abstract class Directive<Value extends DirectiveValue> implements Show {
     @Nonnull public final String name;
 
@@ -63,6 +69,7 @@ public abstract class Directive<Value extends DirectiveValue> implements Show {
         register(WorkerSrcDirective.class);
         register(BlockAllMixedContentDirective.class);
         register(BaseUriDirective.class);
+        register(PrefetchSrcDirective.class);
         FETCH_DIRECIVE_COUNT = fetchDirectives.size();
         NESTED_CONTEXT_DIRECTIVE_COUNT = nestedContextDirectives.size();
     }
diff --git a/src/main/java/com/shapesecurity/salvation/directives/PrefetchSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/PrefetchSrcDirective.java
new file mode 100644
index 0000000..fae1138
--- /dev/null
+++ b/src/main/java/com/shapesecurity/salvation/directives/PrefetchSrcDirective.java
@@ -0,0 +1,19 @@
+package com.shapesecurity.salvation.directives;
+
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import com.shapesecurity.salvation.directiveValues.SourceExpression;
+
+public class PrefetchSrcDirective extends FetchDirective {
+    @Nonnull private static final String NAME = "prefetch-src";
+
+    public PrefetchSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
+        super(PrefetchSrcDirective.NAME, sourceExpressions);
+    }
+
+    @Nonnull @Override public Directive<SourceExpression> construct(Set<SourceExpression> newValues) {
+        return new PrefetchSrcDirective(newValues);
+    }
+}
diff --git a/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java b/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java
index b97252d..12e35ca 100644
--- a/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java
+++ b/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java
@@ -31,6 +31,7 @@ public class DirectiveNameToken extends Token {
         MediaSrc,
         ObjectSrc,
         PluginTypes,
+        PrefetchSrc, // CSP3; in editor's draft at https://w3c.github.io/webappsec-csp/#directive-prefetch-src
         Referrer, // will be removed
         ReportTo, // CSP3; in draft at http://w3c.github.io/webappsec-csp/#directive-report-to
         ReportUri, // CSP3 deprecates it
@@ -76,6 +77,8 @@ public class DirectiveNameToken extends Token {
                     return ObjectSrc;
                 case "plugin-types":
                     return PluginTypes;
+                case "prefetch-src":
+                    return PrefetchSrc;
                 case "referrer":
                     return Referrer;
                 case "report-to":


Test patch:
diff --git a/src/test/java/com/shapesecurity/salvation/ParserTest.java b/src/test/java/com/shapesecurity/salvation/ParserTest.java
index e2ab807..00d67f5 100644
--- a/src/test/java/com/shapesecurity/salvation/ParserTest.java
+++ b/src/test/java/com/shapesecurity/salvation/ParserTest.java
@@ -1,20 +1,30 @@
 package com.shapesecurity.salvation;
 
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
 import com.shapesecurity.salvation.data.Notice;
 import com.shapesecurity.salvation.data.Policy;
 import com.shapesecurity.salvation.data.URI;
-import com.shapesecurity.salvation.directives.*;
+import com.shapesecurity.salvation.directives.Directive;
+import com.shapesecurity.salvation.directives.FrameAncestorsDirective;
+import com.shapesecurity.salvation.directives.ImgSrcDirective;
+import com.shapesecurity.salvation.directives.PluginTypesDirective;
+import com.shapesecurity.salvation.directives.ReportUriDirective;
+import com.shapesecurity.salvation.directives.SandboxDirective;
+import com.shapesecurity.salvation.directives.ScriptSrcDirective;
 import com.shapesecurity.salvation.tokens.DirectiveNameToken;
 import com.shapesecurity.salvation.tokens.DirectiveValueToken;
 import com.shapesecurity.salvation.tokens.Token;
 import org.junit.Test;
-
-import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Scanner;
-
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class ParserTest extends CSPTest {
 
@@ -106,6 +116,14 @@ public class ParserTest extends CSPTest {
         assertNotNull("policy should not be null", p);
         assertEquals("directive count", 1, p.getDirectives().size());
 
+        p = parse("prefetch-src http://*.example.com:*");
+        assertNotNull("policy should not be null", p);
+        assertEquals("directive count", 1, p.getDirectives().size());
+
+        p = parse("prefetch-src samba://*.example.com");
+        assertNotNull("policy should not be null", p);
+        assertEquals("directive count", 1, p.getDirectives().size());
+
         p = parseWithNotices("abc", notices);
         assertEquals(0, p.getDirectives().size());
         assertEquals(1, notices.size());
diff --git a/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java b/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
index 45c1657..eb4873f 100644
--- a/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
+++ b/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
@@ -1,5 +1,9 @@
 package com.shapesecurity.salvation;
 
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
 import com.shapesecurity.salvation.data.Policy;
 import com.shapesecurity.salvation.data.URI;
 import com.shapesecurity.salvation.directiveValues.HostSource;
@@ -9,11 +13,6 @@ import com.shapesecurity.salvation.directives.DefaultSrcDirective;
 import com.shapesecurity.salvation.directives.ScriptSrcDirective;
 import com.shapesecurity.salvation.directives.StyleSrcDirective;
 import org.junit.Test;
-
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
@@ -29,10 +28,10 @@ public class PolicyMergeTest extends CSPTest {
 
         p1 = Parser.parse("default-src d; connect-src a; script-src a; media-src a; ", "https://origin1.com");
         p2 = Parser
-            .parse("default-src; img-src b; style-src b; font-src b; child-src b; object-src b; manifest-src b;", "https://origin2.com");
+            .parse("default-src; img-src b; style-src b; font-src b; child-src b; object-src b; manifest-src b; prefetch-src b;", "https://origin2.com");
         p1.union(p2);
         assertEquals(
-            "connect-src a; script-src a; media-src a; style-src d b; img-src d b; child-src d b; font-src d b; object-src d b; manifest-src d b",
+            "connect-src a; script-src a; media-src a; style-src d b; img-src d b; child-src d b; font-src d b; object-src d b; manifest-src d b; prefetch-src d b",
             p1.show());
     }
 
@@ -148,6 +147,16 @@ public class PolicyMergeTest extends CSPTest {
         p1.intersect(p2);
         assertEquals("default-src 'self' 'strict-dynamic'; script-src a; style-src", p1.show());
 
+        p1 = parse("prefetch-src a");
+        p2 = parse("script-src a; style-src b");
+        p1.intersect(p2);
+        assertEquals("prefetch-src a; script-src a; style-src b", p1.show());
+
+        p1 = parse("prefetch-src a");
+        p2 = parse("prefetch-src a; style-src b");
+        p1.intersect(p2);
+        assertEquals("prefetch-src a; style-src b", p1.show());
+
         p1 = ParserWithLocation.parse("script-src a", URI.parse("https://origin"));
         p2 = parse("script-src b; report-uri /x");
         try {
diff --git a/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java b/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java
index fd502fc..3a8fb25 100644
--- a/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java
+++ b/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java
@@ -1,5 +1,8 @@
 package com.shapesecurity.salvation;
 
+import java.util.Collections;
+import java.util.stream.Stream;
+
 import com.shapesecurity.salvation.data.Base64Value;
 import com.shapesecurity.salvation.data.GUID;
 import com.shapesecurity.salvation.data.Policy;
@@ -10,12 +13,20 @@ import com.shapesecurity.salvation.directiveValues.KeywordSource;
 import com.shapesecurity.salvation.directiveValues.MediaType;
 import com.shapesecurity.salvation.directiveValues.NonceSource;
 import com.shapesecurity.salvation.directiveValues.SourceExpression;
-import com.shapesecurity.salvation.directives.*;
+import com.shapesecurity.salvation.directives.ChildSrcDirective;
+import com.shapesecurity.salvation.directives.ConnectSrcDirective;
+import com.shapesecurity.salvation.directives.DefaultSrcDirective;
+import com.shapesecurity.salvation.directives.DirectiveValue;
+import com.shapesecurity.salvation.directives.FontSrcDirective;
+import com.shapesecurity.salvation.directives.FrameSrcDirective;
+import com.shapesecurity.salvation.directives.ImgSrcDirective;
+import com.shapesecurity.salvation.directives.MediaSrcDirective;
+import com.shapesecurity.salvation.directives.ObjectSrcDirective;
+import com.shapesecurity.salvation.directives.ReportUriDirective;
+import com.shapesecurity.salvation.directives.ScriptSrcDirective;
+import com.shapesecurity.salvation.directives.StyleSrcDirective;
+import com.shapesecurity.salvation.directives.WorkerSrcDirective;
 import org.junit.Test;
-
-import java.util.Collections;
-import java.util.stream.Stream;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -151,6 +162,12 @@ public class PolicyQueryingTest extends CSPTest {
         assertFalse("resource is not allowed", p.allowsManifestFromSource(URI.parse("https://manifest.com:555")));
         assertFalse("resource is not allowed", p.allowsManifestFromSource(URI.parse("http://www.def.am:555")));
         assertFalse("resource is not allowed", p.allowsManifestFromSource(URI.parse("https://someco.net")));
+
+        p = Parser.parse("prefetch-src https://prefetchy.com http://prefetchy.org", URI.parse("https://abc.com"));
+        assertTrue("resource is allowed", p.allowsPrefetchFromSource(URI.parse("https://prefetchy.com")));
+        assertFalse("resource is not allowed", p.allowsPrefetchFromSource(URI.parse("https://prefetchy.com:555")));
+        assertFalse("resource is not allowed", p.allowsPrefetchFromSource(URI.parse("http://www.def.am:555")));
+        assertFalse("resource is not allowed", p.allowsPrefetchFromSource(URI.parse("https://someco.net")));
         
     }
 
diff --git a/src/test/java/com/shapesecurity/salvation/PostProcessTest.java b/src/test/java/com/shapesecurity/salvation/PostProcessTest.java
index 4157578..88d3d2a 100644
--- a/src/test/java/com/shapesecurity/salvation/PostProcessTest.java
+++ b/src/test/java/com/shapesecurity/salvation/PostProcessTest.java
@@ -39,15 +39,15 @@ public class PostProcessTest extends CSPTest {
     }
 
     @Test public void testFetchDirectiveReduce() {
-        Policy p = parse("script-src a; style-src a; img-src a; child-src a; connect-src a; font-src a; media-src a; object-src a; manifest-src a ");
+        Policy p = parse("script-src a; style-src a; img-src a; child-src a; connect-src a; font-src a; media-src a; object-src a; manifest-src a; prefetch-src a ");
         p.postProcessOptimisation();
         assertEquals("default-src a", p.show());
 
-        p = parse("form-action a; script-src a; style-src a; img-src a; child-src a; connect-src a; font-src a; media-src a; object-src a; manifest-src a ");
+        p = parse("form-action a; script-src a; style-src a; img-src a; child-src a; connect-src a; font-src a; media-src a; object-src a; manifest-src a; prefetch-src a ");
         p.postProcessOptimisation();
         assertEquals("form-action a; default-src a", p.show());
 
-        p = parse("script-src a; style-src a; img-src a; child-src a; connect-src a; base-uri a; font-src a; media-src a; object-src a; manifest-src a ");
+        p = parse("script-src a; style-src a; img-src a; child-src a; connect-src a; base-uri a; font-src a; media-src a; object-src a; manifest-src a; prefetch-src a ");
         p.postProcessOptimisation();
         assertEquals("base-uri a; default-src a", p.show());
     }