Repository: shapesecurity/salvation (java)

Issue:
Support `navigation-to` directive
https://w3c.github.io/webappsec-csp/#directive-navigation-to

Pull request:
Support `navigate-to` directive
Fixes #190

Patch:
diff --git a/salvation.iml b/salvation.iml
deleted file mode 100644
index 3933a03..0000000
--- a/salvation.iml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
-    <output url="file://$MODULE_DIR$/target/classes" />
-    <output-test url="file://$MODULE_DIR$/target/test-classes" />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
-      <excludeFolder url="file://$MODULE_DIR$/target" />
-    </content>
-    <orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.findbugs:jsr305:2.0.1" level="project" />
-    <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.11" level="project" />
-    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/src/main/java/com/shapesecurity/salvation/Constants.java b/src/main/java/com/shapesecurity/salvation/Constants.java
index f78e8a1..7996d93 100644
--- a/src/main/java/com/shapesecurity/salvation/Constants.java
+++ b/src/main/java/com/shapesecurity/salvation/Constants.java
@@ -16,7 +16,7 @@ import java.util.regex.Pattern;
             .compile("^(?:script|style)$", Pattern.CASE_INSENSITIVE);
     // RFC 2045 appendix A: productions of type and subtype
     public static final Pattern mediaTypePattern = Pattern.compile("^(?<type>[a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)/(?<subtype>[a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)$");
-    public static final Pattern unquotedKeywordPattern = Pattern.compile("^(?:self|unsafe-inline|unsafe-eval|unsafe-redirect|none|strict-dynamic|unsafe-hashed-attributes|report-sample)$");
+    public static final Pattern unquotedKeywordPattern = Pattern.compile("^(?:self|unsafe-inline|unsafe-eval|unsafe-redirect|none|strict-dynamic|unsafe-hashed-attributes|report-sample|unsafe-allow-redirects)$");
     // port-part constants
     public static final int WILDCARD_PORT = -200;
     public static final int EMPTY_PORT = -1;
diff --git a/src/main/java/com/shapesecurity/salvation/Parser.java b/src/main/java/com/shapesecurity/salvation/Parser.java
index accd475..00b438b 100644
--- a/src/main/java/com/shapesecurity/salvation/Parser.java
+++ b/src/main/java/com/shapesecurity/salvation/Parser.java
@@ -43,9 +43,11 @@ 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;
@@ -105,7 +107,8 @@ public class Parser {
     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 unsafeHashedWithoutHashWarningMessage = "The \"'unsafe-hashed-attributes'\" 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_HASHED_ATTR, SEEN_REPORT_SAMPLE};
+    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_HASHED_ATTR, SEEN_REPORT_SAMPLE, SEEN_UNSAFE_ALLOW_REDIRECTS}
+
     @Nonnull protected final Token[] tokens;
     @Nonnull private final Origin origin;
     protected int index = 0;
@@ -297,6 +300,9 @@ public class Parser {
                 case MediaSrc:
                     result = new MediaSrcDirective(this.parseSourceList());
                     break;
+                case NavigateTo:
+                    result = new NavigateToDirective(this.parseSourceList());
+                    break;
                 case ObjectSrc:
                     result = new ObjectSrcDirective(this.parseSourceList());
                     break;
@@ -449,6 +455,8 @@ public class Parser {
                     seenStates.add(SeenStates.SEEN_UNSAFE_HASHED_ATTR);
                 } 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.");
@@ -503,6 +511,8 @@ public class Parser {
                 return KeywordSource.UnsafeHashedAttributes;
             case "'report-sample'":
                 return KeywordSource.ReportSample;
+            case "'unsafe-allow-redirects'":
+                return KeywordSource.UnsafeAllowRedirects;
             default:
                 checkForUnquotedKeyword(token);
                 if (token.value.startsWith("'nonce-")) {
diff --git a/src/main/java/com/shapesecurity/salvation/Tokeniser.java b/src/main/java/com/shapesecurity/salvation/Tokeniser.java
index 6663bbd..524fe2f 100644
--- a/src/main/java/com/shapesecurity/salvation/Tokeniser.java
+++ b/src/main/java/com/shapesecurity/salvation/Tokeniser.java
@@ -136,6 +136,7 @@ public class Tokeniser {
                     case ImgSrc:
                     case ManifestSrc:
                     case MediaSrc:
+                    case NavigateTo:
                     case ObjectSrc:
                     case PrefetchSrc:
                     case ScriptSrc:
diff --git a/src/main/java/com/shapesecurity/salvation/data/Policy.java b/src/main/java/com/shapesecurity/salvation/data/Policy.java
index d5d8751..37a5425 100644
--- a/src/main/java/com/shapesecurity/salvation/data/Policy.java
+++ b/src/main/java/com/shapesecurity/salvation/data/Policy.java
@@ -26,28 +26,7 @@ import com.shapesecurity.salvation.directiveValues.NonceSource;
 import com.shapesecurity.salvation.directiveValues.None;
 import com.shapesecurity.salvation.directiveValues.SchemeSource;
 import com.shapesecurity.salvation.directiveValues.SourceExpression;
-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.FetchDirective;
-import com.shapesecurity.salvation.directives.FontSrcDirective;
-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.ScriptSrcDirective;
-import com.shapesecurity.salvation.directives.SourceListDirective;
-import com.shapesecurity.salvation.directives.StyleSrcDirective;
-import com.shapesecurity.salvation.directives.WorkerSrcDirective;
+import com.shapesecurity.salvation.directives.*;
 import com.shapesecurity.salvation.interfaces.Show;
 
 public class Policy implements Show {
@@ -814,6 +793,38 @@ public class Policy implements Show {
         }
         return manifestSrcDirective.matchesSource(this.origin, source);
     }
+
+    public boolean allowsNavigation(@Nonnull URI destination) {
+        NavigateToDirective navigateToDirective = this.getDirectiveByType(NavigateToDirective.class);
+        if (navigateToDirective == null) {
+            return true;
+        }
+        return navigateToDirective.matchesSource(origin, destination);
+    }
+
+    public boolean allowsNavigation(@Nonnull GUID destination) {
+        NavigateToDirective navigateToDirective = this.getDirectiveByType(NavigateToDirective.class);
+        if (navigateToDirective == null) {
+            return true;
+        }
+        return navigateToDirective.matchesSource(origin, destination);
+    }
+
+    public boolean allowsFormAction(@Nonnull URI destination) {
+        FormActionDirective formActionDirective = this.getDirectiveByType(FormActionDirective.class);
+        if (formActionDirective == null) {
+            return this.allowsNavigation(destination);
+        }
+        return formActionDirective.matchesSource(this.origin, destination);
+    }
+
+    public boolean allowsFormAction(@Nonnull GUID destination) {
+        FormActionDirective formActionDirective = this.getDirectiveByType(FormActionDirective.class);
+        if (formActionDirective == null) {
+            return this.allowsNavigation(destination);
+        }
+        return formActionDirective.matchesSource(this.origin, destination);
+    }
     
     public boolean hasSomeEffect() {
         for (Map.Entry<Class<?>, Directive<? extends DirectiveValue>> entry : this.directives.entrySet()) {
diff --git a/src/main/java/com/shapesecurity/salvation/directiveValues/KeywordSource.java b/src/main/java/com/shapesecurity/salvation/directiveValues/KeywordSource.java
index 42b5d5b..a0f1752 100644
--- a/src/main/java/com/shapesecurity/salvation/directiveValues/KeywordSource.java
+++ b/src/main/java/com/shapesecurity/salvation/directiveValues/KeywordSource.java
@@ -16,6 +16,7 @@ public class KeywordSource implements SourceExpression, AncestorSource, MatchesS
     @Nonnull public static final KeywordSource StrictDynamic = new KeywordSource("strict-dynamic");
     @Nonnull public static final KeywordSource UnsafeHashedAttributes = new KeywordSource("unsafe-hashed-attributes");
     @Nonnull public static final KeywordSource ReportSample = new KeywordSource("report-sample");
+    @Nonnull public static final KeywordSource UnsafeAllowRedirects = new KeywordSource("unsafe-allow-redirects");
     @Nonnull private final String value;
 
     private KeywordSource(@Nonnull String value) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/Directive.java b/src/main/java/com/shapesecurity/salvation/directives/Directive.java
index 90939f5..e656ece 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/Directive.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/Directive.java
@@ -65,6 +65,7 @@ public abstract class Directive<Value extends DirectiveValue> implements Show {
         register(SandboxDirective.class);
         register(PluginTypesDirective.class);
         register(FormActionDirective.class);
+        register(NavigateToDirective.class);
         register(UpgradeInsecureRequestsDirective.class);
         register(WorkerSrcDirective.class);
         register(BlockAllMixedContentDirective.class);
diff --git a/src/main/java/com/shapesecurity/salvation/directives/NavigateToDirective.java b/src/main/java/com/shapesecurity/salvation/directives/NavigateToDirective.java
new file mode 100644
index 0000000..49d7768
--- /dev/null
+++ b/src/main/java/com/shapesecurity/salvation/directives/NavigateToDirective.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 NavigateToDirective extends SourceListDirective {
+    @Nonnull private static final String name = "navigate-to";
+
+    public NavigateToDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
+        super(NavigateToDirective.name, sourceExpressions);
+    }
+
+    @Nonnull @Override public Directive<SourceExpression> construct(Set<SourceExpression> newValues) {
+        return new NavigateToDirective(newValues);
+    }
+}
diff --git a/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java b/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java
index 12e35ca..91f46f1 100644
--- a/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java
+++ b/src/main/java/com/shapesecurity/salvation/tokens/DirectiveNameToken.java
@@ -29,6 +29,7 @@ public class DirectiveNameToken extends Token {
         ImgSrc,
         ManifestSrc, // CSP3; in draft at http://w3c.github.io/webappsec-csp/#directive-manifest-src as of 2014-10-26
         MediaSrc,
+        NavigateTo,
         ObjectSrc,
         PluginTypes,
         PrefetchSrc, // CSP3; in editor's draft at https://w3c.github.io/webappsec-csp/#directive-prefetch-src
@@ -73,6 +74,8 @@ public class DirectiveNameToken extends Token {
                     return ManifestSrc;
                 case "media-src":
                     return MediaSrc;
+                case "navigate-to":
+                    return NavigateTo;
                 case "object-src":
                     return ObjectSrc;
                 case "plugin-types":


Test patch:
diff --git a/src/test/java/com/shapesecurity/salvation/LocationTest.java b/src/test/java/com/shapesecurity/salvation/LocationTest.java
index 33fd5d4..47e6bbd 100644
--- a/src/test/java/com/shapesecurity/salvation/LocationTest.java
+++ b/src/test/java/com/shapesecurity/salvation/LocationTest.java
@@ -436,9 +436,9 @@ public class LocationTest extends CSPTest {
     @Test public void testPotentialTyposWarnings() {
         ArrayList<Notice> notices = new ArrayList<>();
         ParserWithLocation
-            .parse("script-src unsafe-redirect self none unsafe-inline unsafe-eval", URI.parse("https://origin"),
+            .parse("script-src unsafe-redirect self none unsafe-inline unsafe-eval unsafe-allow-redirects", URI.parse("https://origin"),
                 notices);
-        assertEquals(5, notices.size());
+        assertEquals(6, notices.size());
         Notice notice = notices.get(0);
         assertEquals(
             "1:12: This host name is unusual, and likely meant to be a keyword that is missing the required quotes: 'unsafe-redirect'.",
diff --git a/src/test/java/com/shapesecurity/salvation/ParserTest.java b/src/test/java/com/shapesecurity/salvation/ParserTest.java
index 00d67f5..f70ff03 100644
--- a/src/test/java/com/shapesecurity/salvation/ParserTest.java
+++ b/src/test/java/com/shapesecurity/salvation/ParserTest.java
@@ -72,6 +72,10 @@ public class ParserTest extends CSPTest {
         assertNotNull("policy should not be null", p);
         assertEquals("directive count", 1, p.getDirectives().size());
 
+        p = parse("navigate-to a");
+        assertNotNull("policy should not be null", p);
+        assertEquals("directive count", 1, p.getDirectives().size());
+
         p = parse("frame-src a");
         assertNotNull("policy should not be null", p);
         assertEquals("directive count", 1, p.getDirectives().size());
@@ -124,6 +128,14 @@ public class ParserTest extends CSPTest {
         assertNotNull("policy should not be null", p);
         assertEquals("directive count", 1, p.getDirectives().size());
 
+        p = parse("navigate-to http://*.example.com:*");
+        assertNotNull("policy should not be null", p);
+        assertEquals("directive count", 1, p.getDirectives().size());
+
+        p = parse("navigate-to 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());
@@ -221,6 +233,8 @@ public class ParserTest extends CSPTest {
         assertEquals("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.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 ");
         assertEquals("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.show());
+        p = parse("navigate-to 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 ");
+        assertEquals("navigate-to 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.show());
         p = parse("script-src 'nonce-1234'; style-src 'nonce-1234'");
         assertEquals("script-src 'nonce-1234'; style-src 'nonce-1234'", p.show());
         p = parse("script-src 'nonce-abcd'; style-src 'nonce-1234'");
diff --git a/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java b/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
index f6a9453..e03c12d 100644
--- a/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
+++ b/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
@@ -35,6 +35,24 @@ public class PolicyMergeTest extends CSPTest {
             p1.show());
     }
 
+    @Test public void testUnionNonFetchDirectives() {
+        Policy p1, p2;
+
+        p1 = Parser.parse("form-action aaa; frame-ancestors bbb; navigate-to ccc", "https://origin1.com");
+        p2 = Parser.parse("form-action 'self'; frame-ancestors 'self'; navigate-to 'self'", "https://origin2.com");
+        p1.union(p2);
+        // TODO expand 'self' for ancestor-source-list
+        assertEquals("form-action aaa https://origin2.com; frame-ancestors bbb 'self'; navigate-to ccc https://origin2.com", p1.show());
+
+        p1 = Parser.parse("default-src a ", "https://origin1.com");
+        p2 = Parser
+                .parse("default-src; form-action a; frame-ancestors b; navigate-to c", "https://origin2.com");
+        p1.union(p2);
+        assertEquals(
+                "default-src a; form-action a; frame-ancestors b; navigate-to c",
+                p1.show());
+    }
+
     @Test public void testUnionDefaultSrc() {
         Policy p1, p2;
 
@@ -217,6 +235,55 @@ public class PolicyMergeTest extends CSPTest {
         }
     }
 
+    @Test public void testIntersectNonFetchDirectives() {
+        Policy p1, p2;
+
+        p1 = parse("form-action 'none';");
+        p2 = parse("form-action *;");
+        p1.intersect(p2);
+        assertEquals("form-action", p1.show());
+
+        p1 = parse("frame-ancestors 'none';");
+        p2 = parse("frame-ancestors *;");
+        p1.intersect(p2);
+        assertEquals("frame-ancestors", p1.show());
+
+        p1 = parse("navigate-to 'none';");
+        p2 = parse("navigate-to *;");
+        p1.intersect(p2);
+        assertEquals("navigate-to", p1.show());
+
+        p1 = parse("form-action a;");
+        p2 = parse("form-action a;");
+        p1.intersect(p2);
+        assertEquals("form-action a", p1.show());
+
+        p1 = parse("frame-ancestors a;");
+        p2 = parse("frame-ancestors a;");
+        p1.intersect(p2);
+        assertEquals("frame-ancestors a", p1.show());
+
+        p1 = parse("navigate-to a;");
+        p2 = parse("navigate-to a;");
+        p1.intersect(p2);
+        assertEquals("navigate-to a", p1.show());
+
+        p1 = parse("form-action a b c;");
+        p2 = parse("form-action a;");
+        p1.intersect(p2);
+        assertEquals("form-action a", p1.show());
+
+        p1 = parse("frame-ancestors a b c;");
+        p2 = parse("frame-ancestors a;");
+        p1.intersect(p2);
+        assertEquals("frame-ancestors a", p1.show());
+
+        p1 = parse("navigate-to a b c;");
+        p2 = parse("navigate-to a;");
+        p1.intersect(p2);
+        assertEquals("navigate-to a", p1.show());
+    }
+
     @Test public void testNone() {
         Policy p1, p2;
 
diff --git a/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java b/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java
index 3a8fb25..85828d0 100644
--- a/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java
+++ b/src/test/java/com/shapesecurity/salvation/PolicyQueryingTest.java
@@ -1290,6 +1290,10 @@ public class PolicyQueryingTest extends CSPTest {
         assertTrue(p.allowsFrameFromSource(URI.parse("http://example.com")));
         assertFalse(p.allowsWorkerFromSource(URI.parse("http://example.com")));
         assertFalse(p.allowsScriptFromSource(URI.parse("http://example.com")));
+
+        p = Parser.parse(" child-src blob:", "http://example.com");
+        assertTrue(p.allowsChildFromSource(new GUID("blob:")));
+        assertFalse(p.allowsChildFromSource(new GUID("data:")));
     }
 
     @Test public void testAllowsWorker() {
@@ -1316,5 +1320,48 @@ public class PolicyQueryingTest extends CSPTest {
         assertFalse(p.allowsFrameFromSource(URI.parse("http://example.com")));
         assertTrue(p.allowsWorkerFromSource(URI.parse("http://example.com")));
         assertTrue(p.allowsScriptFromSource(URI.parse("http://example.com")));
+
+        p = Parser.parse(" worker-src blob:", "http://example.com");
+        assertTrue(p.allowsWorkerFromSource(new GUID("blob:")));
+        assertFalse(p.allowsWorkerFromSource(new GUID("data:")));
+    }
+
+    @Test public void testAllowNavigationTo() {
+        Policy p = Parser.parse("navigate-to blob:", "http://example.com");
+        assertTrue(p.allowsNavigation(new GUID("blob:")));
+        assertFalse(p.allowsNavigation(new GUID("data:")));
+        assertTrue(p.allowsFormAction(new GUID("blob:")));
+        assertFalse(p.allowsFormAction(new GUID("data:")));
+
+        p = Parser.parse("navigate-to blob:; form-action data:", "http://example.com");
+        assertTrue(p.allowsNavigation(new GUID("blob:")));
+        assertFalse(p.allowsNavigation(new GUID("data:")));
+        assertTrue(p.allowsFormAction(new GUID("data:")));
+        assertFalse(p.allowsFormAction(new GUID("blob:")));
+
+        p = Parser.parse("form-action data:", "http://example.com");
+        assertTrue(p.allowsNavigation(new GUID("blob:")));
+        assertTrue(p.allowsNavigation(new GUID("data:")));
+        assertTrue(p.allowsFormAction(new GUID("data:")));
+        assertFalse(p.allowsFormAction(new GUID("blob:")));
+
+
+        p = Parser.parse("navigate-to a", "http://example.com");
+        assertTrue(p.allowsNavigation(URI.parse("http://a")));
+        assertFalse(p.allowsNavigation(URI.parse("http://b")));
+        assertTrue(p.allowsFormAction(URI.parse("http://a")));
+        assertFalse(p.allowsFormAction(URI.parse("http://b")));
+
+        p = Parser.parse("navigate-to a; form-action b", "http://example.com");
+        assertTrue(p.allowsNavigation(URI.parse("http://a")));
+        assertFalse(p.allowsNavigation(URI.parse("http://b")));
+        assertTrue(p.allowsFormAction(URI.parse("http://b")));
+        assertFalse(p.allowsFormAction(URI.parse("http://a")));
+
+        p = Parser.parse("form-action a", "http://example.com");
+        assertTrue(p.allowsNavigation(URI.parse("http://a")));
+        assertTrue(p.allowsNavigation(URI.parse("http://b")));
+        assertTrue(p.allowsFormAction(URI.parse("http://a")));
+        assertFalse(p.allowsFormAction(URI.parse("http://b")));
     }
 }