Repository: shapesecurity/salvation (java)

Issue:
Non-empty policy when union merging frame-ancestors and default-src
My unit test:
```java
Policy x = Parser.parse("frame-ancestors https://foo.bar", "http://example.com");
Policy y = Parser.parse("default-src 'none'", "http://example.com");
Policy z = Parser.parse("script-src 'self'; img-src 'self'", "http://example.com");
x.union(y);
x.union(z);
assertEquals("frame-ancestors https://foo.bar; default-src; script-src 'self'; img-src 'self'", x.show());
```

**Salvation 2.5.0 - unit test passes:**
```
Expected :frame-ancestors https://foo.bar; default-src; script-src 'self'; img-src 'self'
Actual   :frame-ancestors https://foo.bar; default-src; script-src 'self'; img-src 'self'
```

**Salvation 2.6.0 - unit test fails:**
```
Expected :frame-ancestors https://foo.bar; default-src; script-src 'self'; img-src 'self'
Actual   :frame-ancestors https://foo.bar
```

While in salvation 2.5.0 merge operation seems to be associative, it seems not to be in 2.6.0.

**Salvation 2.5.0:**
`(x ∪ y) ∪ z = x ∪ (y ∪ z)
`

**Salvation 2.6.0:**
`(x ∪ y) ∪ z ≠ x ∪ (y ∪ z)
`

| (x ∪ y) ∪ z |  x ∪ (y ∪ z) |
| ------------- |-------------|
| [cspvalidator.org](https://cspvalidator.org/#headerValue%5B%5D=frame-ancestors+https%3A%2F%2Ffoo.bar&headerValue%5B%5D=default-src+'none'&headerValue%5B%5D=script-src+'self'%3B+img-src+'self'&strategy=union) | [cspvalidator.org](https://cspvalidator.org/#headerValue%5B%5D=default-src+'none'&headerValue%5B%5D=frame-ancestors+https%3A%2F%2Ffoo.bar&headerValue%5B%5D=script-src+'self'%3B+img-src+'self'&strategy=union) |

_Originally posted by @cnsgithub in https://github.com/shapesecurity/salvation/issues/210#issuecomment-436885547_

Pull request description:
Fixes #213
and tests commutativity

Patch:
diff --git a/src/main/java/com/shapesecurity/salvation/data/Policy.java b/src/main/java/com/shapesecurity/salvation/data/Policy.java
index 13fb42d..923d2c5 100644
--- a/src/main/java/com/shapesecurity/salvation/data/Policy.java
+++ b/src/main/java/com/shapesecurity/salvation/data/Policy.java
@@ -8,7 +8,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -52,31 +51,26 @@ public class Policy implements Show {
 	}
 
 	public void intersect(@Nonnull Policy other) {
-		this.mergeUsingStrategy(other, this::intersectDirectivePrivate);
-	}
+		this.checkForMergeValidity();
+		other.checkForMergeValidity();
 
-	public void union(@Nonnull Policy other) {
-		this.mergeUsingStrategy(other, this::unionDirectivePrivate);
-	}
+		this.resolveSelf();
+		other.resolveSelf();
 
-	private void mergeUsingStrategy(@Nonnull Policy other, Consumer<Directive<? extends DirectiveValue>> strategy) {
-		if (this.directives.containsKey(ReportUriDirective.class) || other.directives
-				.containsKey(ReportUriDirective.class)) {
-			throw new IllegalArgumentException(
-					"Cannot merge policies if either policy contains a report-uri directive.");
-		}
+		this.expandDefaultSrc();
+		other.expandDefaultSrc();
 
-		if (this.directives.containsKey(ReportToDirective.class) || other.directives
-				.containsKey(ReportToDirective.class)) {
-			throw new IllegalArgumentException(
-					"Cannot merge policies if either policy contains a report-to directive.");
+		for (Map.Entry<Class<?>, Directive<? extends DirectiveValue>> entry : other.directives.entrySet()) {
+			this.intersectDirectivePrivate(entry.getValue());
 		}
 
-		if (this.directives.containsKey(ReferrerDirective.class) || other.directives
-				.containsKey(ReferrerDirective.class)) {
-			throw new IllegalArgumentException(
-					"Cannot merge policies if either policy contains a referrer directive.");
-		}
+		this.optimise();
+		other.optimise();
+	}
+
+	public void union(@Nonnull Policy other) {
+		this.checkForMergeValidity();
+		other.checkForMergeValidity();
 
 		this.resolveSelf();
 		other.resolveSelf();
@@ -84,9 +78,27 @@ public class Policy implements Show {
 		this.expandDefaultSrc();
 		other.expandDefaultSrc();
 
-		other.getDirectives().forEach(strategy);
+		for (Map.Entry<Class<?>, Directive<? extends DirectiveValue>> entry : other.directives.entrySet()) {
+			this.unionDirectivePrivate(entry.getValue());
+		}
+		this.directives.entrySet().removeIf(entry -> !other.directives.containsKey(entry.getKey()));
 
 		this.optimise();
+		other.optimise();
+	}
+
+	private void checkForMergeValidity() {
+		if (this.directives.containsKey(ReportUriDirective.class)) {
+			throw new IllegalArgumentException("Cannot merge policies if either policy contains a report-uri directive.");
+		}
+
+		if (this.directives.containsKey(ReportToDirective.class)) {
+			throw new IllegalArgumentException("Cannot merge policies if either policy contains a report-to directive.");
+		}
+
+		if (this.directives.containsKey(ReferrerDirective.class)) {
+			throw new IllegalArgumentException("Cannot merge policies if either policy contains a referrer directive.");
+		}
 	}
 
 	private void resolveSelf() {
@@ -106,34 +118,34 @@ public class Policy implements Show {
 			defaultSources = defaultSrcDirective.values().collect(Collectors.toCollection(LinkedHashSet::new));
 
 			if (!this.directives.containsKey(ScriptSrcDirective.class)) {
-				this.unionDirectivePrivate(new ScriptSrcDirective(defaultSources));
+				this.directives.put(ScriptSrcDirective.class, new ScriptSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(StyleSrcDirective.class)) {
-				this.unionDirectivePrivate(new StyleSrcDirective(defaultSources));
+				this.directives.put(StyleSrcDirective.class, new StyleSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(ImgSrcDirective.class)) {
-				this.unionDirectivePrivate(new ImgSrcDirective(defaultSources));
+				this.directives.put(ImgSrcDirective.class, new ImgSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(ChildSrcDirective.class)) {
-				this.unionDirectivePrivate(new ChildSrcDirective(defaultSources));
+				this.directives.put(ChildSrcDirective.class, new ChildSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(ConnectSrcDirective.class)) {
-				this.unionDirectivePrivate(new ConnectSrcDirective(defaultSources));
+				this.directives.put(ConnectSrcDirective.class, new ConnectSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(FontSrcDirective.class)) {
-				this.unionDirectivePrivate(new FontSrcDirective(defaultSources));
+				this.directives.put(FontSrcDirective.class, new FontSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(MediaSrcDirective.class)) {
-				this.unionDirectivePrivate(new MediaSrcDirective(defaultSources));
+				this.directives.put(MediaSrcDirective.class, new MediaSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(ObjectSrcDirective.class)) {
-				this.unionDirectivePrivate(new ObjectSrcDirective(defaultSources));
+				this.directives.put(ObjectSrcDirective.class, new ObjectSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(ManifestSrcDirective.class)) {
-				this.unionDirectivePrivate(new ManifestSrcDirective(defaultSources));
+				this.directives.put(ManifestSrcDirective.class, new ManifestSrcDirective(defaultSources));
 			}
 			if (!this.directives.containsKey(PrefetchSrcDirective.class)) {
-				this.unionDirectivePrivate(new PrefetchSrcDirective(defaultSources));
+				this.directives.put(PrefetchSrcDirective.class, new PrefetchSrcDirective(defaultSources));
 			}
 		}
 
@@ -141,14 +153,14 @@ public class Policy implements Show {
 		if (this.directives.containsKey(ChildSrcDirective.class) && !this.directives.containsKey(FrameSrcDirective.class)) {
 			ChildSrcDirective childSrcDirective = this.getDirectiveByType(ChildSrcDirective.class);
 			Set<SourceExpression> childSources = childSrcDirective.values().collect(Collectors.toCollection(LinkedHashSet::new));
-			this.unionDirectivePrivate(new FrameSrcDirective(childSources));
+			this.directives.put(FrameSrcDirective.class, new FrameSrcDirective(childSources));
 		}
 
 		// expand script-src
 		if (this.directives.containsKey(ScriptSrcDirective.class) && !this.directives.containsKey(WorkerSrcDirective.class)) {
 			ScriptSrcDirective scriptSrcDirective = this.getDirectiveByType(ScriptSrcDirective.class);
 			Set<SourceExpression> scriptSources = scriptSrcDirective.values().collect(Collectors.toCollection(LinkedHashSet::new));
-			this.unionDirectivePrivate(new WorkerSrcDirective(scriptSources));
+			this.directives.put(WorkerSrcDirective.class, new WorkerSrcDirective(scriptSources));
 		}
 	}
 
@@ -338,10 +350,6 @@ public class Policy implements Show {
 		@SuppressWarnings("unchecked") T oldDirective = (T) this.directives.get(directive.getClass());
 		if (oldDirective != null) {
 			oldDirective.union(directive);
-		} else {
-			if (!(directive instanceof FetchDirective) || this.containsFetchDirective()) {
-				this.directives.put(directive.getClass(), directive);
-			}
 		}
 	}
 


Test patch:
diff --git a/src/test/java/com/shapesecurity/salvation/ParserTest.java b/src/test/java/com/shapesecurity/salvation/ParserTest.java
index 659090f..778aae4 100644
--- a/src/test/java/com/shapesecurity/salvation/ParserTest.java
+++ b/src/test/java/com/shapesecurity/salvation/ParserTest.java
@@ -346,11 +346,11 @@ public class ParserTest extends CSPTest {
 
 		Policy c = parse("script-src *");
 		b.union(c);
-		assertEquals("policy union", "style-src *; script-src *", b.show());
+		assertEquals("policy union", "", b.show());
 
 		Policy d = parse("script-src abc");
 		b.union(d);
-		assertEquals("policy union", "style-src *; script-src *", b.show());
+		assertEquals("policy union", "", b.show());
 
 		a.setOrigin(URI.parse("http://qwe.zz:80"));
 		assertEquals("policy origin", "http://qwe.zz", a.getOrigin().show());
diff --git a/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java b/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
index 68fd342..b20460d 100644
--- a/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
+++ b/src/test/java/com/shapesecurity/salvation/PolicyMergeTest.java
@@ -15,6 +15,7 @@ import com.shapesecurity.salvation.directives.StyleSrcDirective;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class PolicyMergeTest extends CSPTest {
@@ -52,18 +53,18 @@ public class PolicyMergeTest extends CSPTest {
 				.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",
+				"default-src a",
 				p1.show());
 
 		p1 = Parser.parse("form-action aaa; frame-ancestors bbb; navigate-to ccc", "https://origin1.com");
 		p2 = Parser.parse("script-src a", "https://origin1.com");
 		p1.union(p2);
-		assertEquals("form-action aaa; frame-ancestors bbb; navigate-to ccc", p1.show());
+		assertEquals("", p1.show());
 
 		p1 = Parser.parse("frame-ancestors bbb;", "https://origin1.com");
 		p2 = Parser.parse("script-src a", "https://origin1.com");
 		p1.union(p2);
-		assertEquals("frame-ancestors bbb", p1.show());
+		assertEquals("", p1.show());
 
 		p1 = Parser.parse("", "https://origin1.com");
 		p2 = Parser.parse("script-src a", "https://origin1.com");
@@ -580,19 +581,19 @@ public class PolicyMergeTest extends CSPTest {
 		q = parse("worker-src a");
 		p.union(q);
 		p.postProcessOptimisation();
-		assertEquals("child-src a; worker-src a", p.show());
+		assertEquals("", p.show());
 
 		p = parse("child-src a; ");
-		q = parse("worker-src a ");
+		q = parse("frame-src a ");
 		p.union(q);
 		p.postProcessOptimisation();
-		assertEquals("child-src a; worker-src a", p.show());
+		assertEquals("frame-src a", p.show());
 
 		p = parse("child-src a b");
 		q = parse("child-src; worker-src x; frame-src y");
 		p.union(q);
 		p.postProcessOptimisation();
-		assertEquals("child-src a b; frame-src a b y; worker-src x", p.show());
+		assertEquals("child-src a b; frame-src a b y", p.show());
 
 		p = parse("child-src *; worker-src");
 		q = parse("child-src; worker-src b");
@@ -607,7 +608,7 @@ public class PolicyMergeTest extends CSPTest {
 		p = parse("child-src a");
 		q = parse("child-src; worker-src b");
 		p.union(q);
-		assertEquals("child-src a; worker-src b", p.show());
+		assertEquals("child-src a", p.show());
 
 		p = parse("child-src a; worker-src b");
 		q = parse("child-src; worker-src c");
@@ -617,26 +618,36 @@ public class PolicyMergeTest extends CSPTest {
 		p = parse("child-src; worker-src a; frame-src b");
 		q = parse("child-src c");
 		p.union(q);
-		assertEquals("child-src c; worker-src a; frame-src b c", p.show());
+		assertEquals("child-src c; frame-src b c", p.show());
 
 		p = parse("child-src a; worker-src b");
 		q = parse("child-src c; frame-src d");
 		p.union(q);
-		assertEquals("child-src a c; worker-src b; frame-src a d", p.show());
+		assertEquals("child-src a c; frame-src a d", p.show());
 
 		p = parse("child-src b; worker-src a");
 		q = parse("child-src a");
 		p.union(q);
-		assertEquals("child-src b a; worker-src a", p.show());
+		assertEquals("child-src b a", p.show());
 
 		p = parse("default-src a");
 		q = parse("worker-src b; frame-src b;");
 		p.union(q);
-		assertEquals("default-src a; frame-src a b; worker-src a b", p.show());
+		assertEquals("frame-src a b; worker-src a b", p.show());
 	}
 
 	@Test
 	public void testUnionNone() {
+		Policy x = Parser.parse("frame-ancestors https://foo.bar", "http://example.com");
+		Policy y = Parser.parse("default-src 'none'", "http://example.com");
+		x.union(y);
+		assertEquals("", x.show());
+
+		x = Parser.parse("frame-ancestors https://foo.bar", "http://example.com");
+		y = Parser.parse("default-src 'none'; frame-ancestors;", "http://example.com");
+		x.union(y);
+		assertEquals("frame-ancestors https://foo.bar", x.show());
+
 		Policy p = parse("frame-ancestors 'none'");
 		Policy q = parse("frame-ancestors 'self'");
 		p.union(q);
@@ -645,7 +656,7 @@ public class PolicyMergeTest extends CSPTest {
 		p = parse("frame-ancestors 'none' 'none'");
 		q = parse("frame-ancestors 'self'");
 		p.union(q);
-		assertEquals("frame-ancestors 'self'", p.show());
+		assertEquals("", p.show());
 
 		p = parse("frame-ancestors 'self'");
 		q = parse("frame-ancestors 'none'");
@@ -697,4 +708,53 @@ public class PolicyMergeTest extends CSPTest {
 		p.union(q);
 		assertEquals("script-src *", p.show());
 	}
+
+	@Test
+	public void testMergeCommutativity() {
+		String[] policies = new String[] {
+			"script-src 'self'",
+			"script-src 'none'",
+			"script-src a",
+			"script-src a custom:",
+			"style-src 'self'",
+			"style-src 'none'",
+			"style-src a",
+			"style-src a custom:",
+			"default-src 'none'",
+			"default-src a",
+			"default-src custom:",
+			"plugin-types a/b",
+			"frame-ancestors 'self'",
+			"frame-ancestors 'none'",
+			"frame-ancestors a",
+			"frame-ancestors a custom:",
+			"frame-ancestors custom:",
+			"upgrade-insecure-requests",
+			"script-src a; frame-ancestors b"
+		};
+
+		for (int i = 0; i < policies.length; i++) {
+			for (int k = 0; k < policies.length; k++) {
+				Policy pq = parse(policies[i]);
+				pq.union(parse(policies[k]));
+
+				Policy qp = parse(policies[k]);
+				qp.union(parse(policies[i]));
+
+				assertTrue(pq.show() + " ≠ " + qp.show(), pq.equals(qp));
+			}
+		}
+
+		for (int i = 0; i < policies.length; i++) {
+			for (int k = 0; k < policies.length; k++) {
+				Policy pq = parse(policies[i]);
+				pq.intersect(parse(policies[k]));
+
+				Policy qp = parse(policies[k]);
+				qp.intersect(parse(policies[i]));
+
+				assertTrue(pq.show() + " ≠ " + qp.show(), pq.equals(qp));
+			}
+		}
+	}
 }