Repository: shapesecurity/salvation
Pull request #142 (2016-05-05 04:32:07)

fixes GH-138
add a policy method to do a post-process optimisation

shekyan: Addressed comments.
shekyan: Done

Patch:
diff --git a/src/main/java/com/shapesecurity/salvation/Constants.java b/src/main/java/com/shapesecurity/salvation/Constants.java
index 36df416..e6339c2 100644
--- a/src/main/java/com/shapesecurity/salvation/Constants.java
+++ b/src/main/java/com/shapesecurity/salvation/Constants.java
@@ -30,5 +30,4 @@ import java.util.regex.Pattern;
             + ")?" + queryFragmentPart + "$");
     public static final Pattern relativeReportUriPattern =
         Pattern.compile("^(?<path>" + pathPart + ")" + queryFragmentPart + "$");
-
 }
diff --git a/src/main/java/com/shapesecurity/salvation/data/Policy.java b/src/main/java/com/shapesecurity/salvation/data/Policy.java
index 2ffb524..99d80bf 100644
--- a/src/main/java/com/shapesecurity/salvation/data/Policy.java
+++ b/src/main/java/com/shapesecurity/salvation/data/Policy.java
@@ -9,6 +9,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.util.*;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 public class Policy implements Show {
@@ -176,15 +177,8 @@ public class Policy implements Show {
 
 
             // * remove source directives that are equivalent to default-src
-            this.eliminateRedundantSourceExpression(defaultSources, ScriptSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, StyleSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, ImgSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, ChildSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, ConnectSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, FontSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, MediaSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, ObjectSrcDirective.class);
-            this.eliminateRedundantSourceExpression(defaultSources, ManifestSrcDirective.class);
+
+            Directive.getFetchDirectives().forEach(x -> this.eliminateRedundantSourceExpression(defaultSources, x));
 
             // * remove default-src nonces if the policy contains both script-src and style-src directives
             if (this.directives.containsKey(ScriptSrcDirective.class) && this.directives
@@ -195,20 +189,68 @@ public class Policy implements Show {
             }
 
             // * remove unnecessary default-src directives if all source directives exist
-            if (this.directives.containsKey(ScriptSrcDirective.class) &&
-                this.directives.containsKey(StyleSrcDirective.class) &&
-                this.directives.containsKey(ImgSrcDirective.class) &&
-                this.directives.containsKey(ChildSrcDirective.class) &&
-                this.directives.containsKey(ConnectSrcDirective.class) &&
-                this.directives.containsKey(FontSrcDirective.class) &&
-                this.directives.containsKey(MediaSrcDirective.class) &&
-                this.directives.containsKey(ObjectSrcDirective.class) &&
-                this.directives.containsKey(ManifestSrcDirective.class)) {
+            if (all(Directive.getFetchDirectives(), this.directives::containsKey)) {
                 this.directives.remove(DefaultSrcDirective.class);
             }
         }
     }
 
+    private static <A> boolean all(List<A> list, Function<A, Boolean> predicate) {
+        for (A a : list) if (!predicate.apply(a)) return false;
+        return true;
+    }
+
+    public void postProcessOptimisation() {
+
+        DefaultSrcDirective defaultSrcDirective;
+
+        int directiveCount = this.directives.size();
+
+        // below optimisations kick in when there are two or more directives
+        if (directiveCount < 2) {
+            return;
+        }
+        // move source-expressions to default-src if they exist in every fetch directive
+        SourceListDirective prevDirective = null;
+        int fetchDirectiveCount = 0;
+        boolean isfetchSourceListIdentical = true;
+        for (Map.Entry<Class<?>, Directive<? extends DirectiveValue>> entry : this.directives.entrySet()) {
+            Directive<? extends DirectiveValue> directive = entry.getValue();
+            if (directive instanceof FetchDirective) {
+                fetchDirectiveCount++;
+                if (prevDirective != null) {
+                    if (!prevDirective.sourceListEquals(directive)) {
+                        isfetchSourceListIdentical = false;
+                        break;
+                    }
+                }
+                prevDirective = (SourceListDirective) directive;
+            }
+        }
+
+        // remove all fetch directives and replace with default-src
+        if (fetchDirectiveCount == Directive.FETCH_DIRECIVE_COUNT && isfetchSourceListIdentical) {
+            Set<SourceExpression> combinedSources = prevDirective.values().collect(Collectors.toCollection(LinkedHashSet::new));
+            defaultSrcDirective = new DefaultSrcDirective(combinedSources);
+            Directive.getFetchDirectives().forEach(x -> this.directives.remove(x));
+            this.directives.put(DefaultSrcDirective.class, defaultSrcDirective);
+        }
+
+        // if policy contains only fetch directives and those are script-src and style-src with identical source-lists,
+        // and source-lists contain solely keyword-source or nonce-source, source-list can be moved to default-src
+        if (directiveCount == 2 && this.directives.containsKey(ScriptSrcDirective.class) && this.directives
+            .containsKey(StyleSrcDirective.class)) {
+            ScriptSrcDirective scriptSrcDirective = this.getDirectiveByType(ScriptSrcDirective.class);
+            StyleSrcDirective styleSrcDirective = this.getDirectiveByType(StyleSrcDirective.class);
+            if (scriptSrcDirective.sourceListEquals(styleSrcDirective) && scriptSrcDirective.containsKeywordsAndNoncesOnly()) {
+                defaultSrcDirective = new DefaultSrcDirective(scriptSrcDirective.values().collect(Collectors.toCollection(LinkedHashSet::new)));
+                this.directives.remove(ScriptSrcDirective.class);
+                this.directives.remove(StyleSrcDirective.class);
+                this.directives.put(DefaultSrcDirective.class, defaultSrcDirective);
+            }
+        }
+    }
+
     public void unionDirective(@Nonnull Directive<? extends DirectiveValue> directive) {
         this.resolveSelf();
         if (directive instanceof SourceListDirective) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/ChildSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/ChildSrcDirective.java
index 5962a2b..2582a5a 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/ChildSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/ChildSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class ChildSrcDirective extends SourceListDirective {
+public class ChildSrcDirective extends FetchDirective {
     @Nonnull private static final String NAME = "child-src";
 
     public ChildSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/ConnectSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/ConnectSrcDirective.java
index ca2d895..be19993 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/ConnectSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/ConnectSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public final class ConnectSrcDirective extends SourceListDirective {
+public final class ConnectSrcDirective extends FetchDirective {
     @Nonnull private static final String NAME = "connect-src";
 
     public ConnectSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/DefaultSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/DefaultSrcDirective.java
index 74b4933..332245b 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/DefaultSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/DefaultSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class DefaultSrcDirective extends SourceListDirective {
+public class DefaultSrcDirective extends FetchDirective {
     @Nonnull private static final String name = "default-src";
 
     public DefaultSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/Directive.java b/src/main/java/com/shapesecurity/salvation/directives/Directive.java
index 09d2225..25e814d 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/Directive.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/Directive.java
@@ -6,10 +6,7 @@ import com.shapesecurity.salvation.interfaces.Show;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -19,6 +16,46 @@ public abstract class Directive<Value extends DirectiveValue> implements Show {
 
     @Nonnull private Set<Value> values;
 
+    public static List<Class<? extends Directive>> getFetchDirectives() {
+        return fetchDirectives;
+    }
+
+    static List<Class<? extends Directive>> fetchDirectives = new ArrayList<>();
+    static public final int FETCH_DIRECIVE_COUNT;
+
+    static void register(Class<? extends Directive> directiveClass) {
+        if (FetchDirective.class.isAssignableFrom(directiveClass) && directiveClass != DefaultSrcDirective.class) {
+            fetchDirectives.add(directiveClass);
+        }
+    }
+
+    static {
+        // NOTE: new direvctive types must be registered here
+        register(ScriptSrcDirective.class);
+        register(StyleSrcDirective.class);
+        register(ImgSrcDirective.class);
+        register(ChildSrcDirective.class);
+        register(ConnectSrcDirective.class);
+        register(FontSrcDirective.class);
+        register(MediaSrcDirective.class);
+        register(ObjectSrcDirective.class);
+        register(ManifestSrcDirective.class);
+        register(DefaultSrcDirective.class);
+        register(ReferrerDirective.class);
+        register(FrameSrcDirective.class);
+        register(ReportUriDirective.class);
+        register(ReportToDirective.class);
+        register(FrameAncestorsDirective.class);
+        register(SandboxDirective.class);
+        register(PluginTypesDirective.class);
+        register(FormActionDirective.class);
+        register(UpgradeInsecureRequestsDirective.class);
+        register(BlockAllMixedContentDirective.class);
+        register(BaseUriDirective.class);
+        FETCH_DIRECIVE_COUNT = fetchDirectives.size();
+
+    }
+
     Directive(@Nonnull String name, @Nonnull Set<Value> values) {
         this.name = name;
         this.values = values;
@@ -128,6 +165,10 @@ public abstract class Directive<Value extends DirectiveValue> implements Show {
         return values().anyMatch(value::equals);
     }
 
+    public final boolean sourceListEquals(@Nullable Object other) {
+        return other != null && this.equalsHelper((Directive<Value>) other);
+    }
+
     @SuppressWarnings("unchecked") @Override public final boolean equals(@Nullable Object other) {
         return other != null && other.getClass() == this.getClass() && this.equalsHelper((Directive<Value>) other);
     }
diff --git a/src/main/java/com/shapesecurity/salvation/directives/FetchDirective.java b/src/main/java/com/shapesecurity/salvation/directives/FetchDirective.java
new file mode 100644
index 0000000..310a755
--- /dev/null
+++ b/src/main/java/com/shapesecurity/salvation/directives/FetchDirective.java
@@ -0,0 +1,13 @@
+package com.shapesecurity.salvation.directives;
+
+import com.shapesecurity.salvation.directiveValues.SourceExpression;
+
+import javax.annotation.Nonnull;
+import java.util.Set;
+
+public abstract class FetchDirective extends SourceListDirective {
+
+    public FetchDirective(@Nonnull String name, @Nonnull Set<SourceExpression> sourceExpressions) {
+        super(name, sourceExpressions);
+    }
+}
diff --git a/src/main/java/com/shapesecurity/salvation/directives/FontSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/FontSrcDirective.java
index 4583ba2..ab6c903 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/FontSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/FontSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class FontSrcDirective extends SourceListDirective {
+public class FontSrcDirective extends FetchDirective {
     @Nonnull private static final String name = "font-src";
 
     public FontSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/ImgSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/ImgSrcDirective.java
index 1a58c5e..a01db70 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/ImgSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/ImgSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class ImgSrcDirective extends SourceListDirective {
+public class ImgSrcDirective extends FetchDirective {
     @Nonnull public static final String name = "img-src";
 
     public ImgSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/ManifestSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/ManifestSrcDirective.java
index 4c916f5..d60c33b 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/ManifestSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/ManifestSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class ManifestSrcDirective extends SourceListDirective {
+public class ManifestSrcDirective extends FetchDirective {
     @Nonnull private static final String name = "manifest-src";
 
     public ManifestSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/MediaSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/MediaSrcDirective.java
index d97bda9..f5a17e1 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/MediaSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/MediaSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class MediaSrcDirective extends SourceListDirective {
+public class MediaSrcDirective extends FetchDirective {
     @Nonnull private static final String name = "media-src";
 
     public MediaSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/ObjectSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/ObjectSrcDirective.java
index f6b4cba..5350fab 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/ObjectSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/ObjectSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class ObjectSrcDirective extends SourceListDirective {
+public class ObjectSrcDirective extends FetchDirective {
     @Nonnull private static final String name = "object-src";
 
     public ObjectSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/ScriptSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/ScriptSrcDirective.java
index 0b3827c..d525b39 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/ScriptSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/ScriptSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class ScriptSrcDirective extends SourceListDirective {
+public class ScriptSrcDirective extends FetchDirective {
     @Nonnull private static final String NAME = "script-src";
 
     public ScriptSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/SourceListDirective.java b/src/main/java/com/shapesecurity/salvation/directives/SourceListDirective.java
index 535631c..8c68ea6 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/SourceListDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/SourceListDirective.java
@@ -44,6 +44,10 @@ public abstract class SourceListDirective extends Directive<SourceExpression>
         return this.values().anyMatch(x -> (x instanceof NonceSource));
     }
 
+    public boolean containsKeywordsAndNoncesOnly() {
+        return this.values().allMatch(x -> (x instanceof KeywordSource || x instanceof NonceSource));
+    }
+
     @Nonnull public Directive<SourceExpression> resolveSelf(@Nonnull Origin origin) {
         return this.bind(dv -> {
             if (dv == KeywordSource.Self) {
diff --git a/src/main/java/com/shapesecurity/salvation/directives/StyleSrcDirective.java b/src/main/java/com/shapesecurity/salvation/directives/StyleSrcDirective.java
index fd63e50..3ba23d3 100644
--- a/src/main/java/com/shapesecurity/salvation/directives/StyleSrcDirective.java
+++ b/src/main/java/com/shapesecurity/salvation/directives/StyleSrcDirective.java
@@ -5,7 +5,7 @@ import com.shapesecurity.salvation.directiveValues.SourceExpression;
 import javax.annotation.Nonnull;
 import java.util.Set;
 
-public class StyleSrcDirective extends SourceListDirective {
+public class StyleSrcDirective extends FetchDirective {
     @Nonnull private static final String NAME = "style-src";
 
     public StyleSrcDirective(@Nonnull Set<SourceExpression> sourceExpressions) {


Test patch:
diff --git a/src/test/java/com/shapesecurity/salvation/ParserTest.java b/src/test/java/com/shapesecurity/salvation/ParserTest.java
index 1a96d36..f654929 100644
--- a/src/test/java/com/shapesecurity/salvation/ParserTest.java
+++ b/src/test/java/com/shapesecurity/salvation/ParserTest.java
@@ -1,10 +1,8 @@
 package com.shapesecurity.salvation;
 
-import com.shapesecurity.salvation.data.Base64Value;
 import com.shapesecurity.salvation.data.Notice;
 import com.shapesecurity.salvation.data.Policy;
 import com.shapesecurity.salvation.data.URI;
-import com.shapesecurity.salvation.directiveValues.HashSource;
 import com.shapesecurity.salvation.directives.*;
 import com.shapesecurity.salvation.tokens.DirectiveNameToken;
 import com.shapesecurity.salvation.tokens.DirectiveValueToken;
@@ -196,6 +194,21 @@ public class ParserTest extends CSPTest {
         assertEquals("optimisation", "script-src 'nonce-123' *", parseAndShow("script-src example.com * 'unsafe-inline' 'nonce-123'"));
         assertEquals("optimisation", "script-src 'sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=' *", parseAndShow("script-src example.com * 'unsafe-inline' 'sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols='"));
         assertEquals("optimisation", "script-src 'sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=' 'nonce-123' *", parseAndShow("script-src example.com * 'sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=' 'nonce-123' 'unsafe-inline'"));
+
+        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 ");
+        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("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'");
+        assertEquals("script-src 'nonce-abcd'; style-src 'nonce-1234'", p.show());
+        p = parse("script-src 'nonce-abcd' 'nonce-1234'; style-src 'nonce-1234'");
+        assertEquals("script-src 'nonce-abcd' 'nonce-1234'; style-src 'nonce-1234'", p.show());
+        p = parse("script-src a 'nonce-1234'; style-src a 'nonce-1234'");
+        assertEquals("script-src a 'nonce-1234'; style-src a 'nonce-1234'", p.show());
+        p = parse("script-src 'nonce-1234'; style-src 'nonce-1234'; default-src a");
+        assertEquals("script-src 'nonce-1234'; style-src 'nonce-1234'; default-src a", p.show());
     }
 
     @Test public void testAncestorSource() {
diff --git a/src/test/java/com/shapesecurity/salvation/PostProcessTest.java b/src/test/java/com/shapesecurity/salvation/PostProcessTest.java
new file mode 100644
index 0000000..4157578
--- /dev/null
+++ b/src/test/java/com/shapesecurity/salvation/PostProcessTest.java
@@ -0,0 +1,55 @@
+package com.shapesecurity.salvation;
+
+import com.shapesecurity.salvation.data.Policy;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class PostProcessTest extends CSPTest {
+
+    @Test public void testKeywordReduce() {
+        Policy p;
+        p = parse("script-src 'unsafe-inline'; style-src 'unsafe-inline';");
+        p.postProcessOptimisation();
+        assertEquals("default-src 'unsafe-inline'", p.show());
+
+        p = parse("script-src 'unsafe-eval'; style-src 'unsafe-eval';");
+        p.postProcessOptimisation();
+        assertEquals("default-src 'unsafe-eval'", p.show());
+
+        p = parse("script-src 'unsafe-eval' 'nonce-123'; style-src 'unsafe-eval' 'nonce-123';");
+        p.postProcessOptimisation();
+        assertEquals("default-src 'unsafe-eval' 'nonce-123'", p.show());
+
+        p = parse("script-src 'unsafe-eval' 'nonce-123'; style-src 'unsafe-eval';");
+        p.postProcessOptimisation();
+        assertEquals("script-src 'unsafe-eval' 'nonce-123'; style-src 'unsafe-eval'", p.show());
+
+        p = parse("script-src 'self'; style-src 'self';");
+        p.postProcessOptimisation();
+        assertEquals("default-src 'self'", p.show());
+
+        p = parse("script-src 'nonce-Q-ecAIccSGatv6lJrCBVARPr'; style-src 'nonce-Q-ecAIccSGatv6lJrCBVARPr'");
+        p.postProcessOptimisation();
+        assertEquals("default-src 'nonce-Q-ecAIccSGatv6lJrCBVARPr'", p.show());
+
+        p = parse("script-src 'nonce-1234'; style-src 'nonce-1234'; default-src a");
+        p.postProcessOptimisation();
+        assertEquals("script-src 'nonce-1234'; style-src 'nonce-1234'; default-src a", p.show());
+    }
+
+    @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 ");
+        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.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.postProcessOptimisation();
+        assertEquals("base-uri a; default-src a", p.show());
+    }
+
+}