From b029fb3a18e6358edcb9a3a253bff7b53f3591e3 Mon Sep 17 00:00:00 2001 From: Sergey Shekyan Date: Thu, 29 Mar 2018 10:28:37 -0700 Subject: [PATCH] Support `navigate-to` directive (#192) * Support `navigation-to` directive Fixes #190 * Typo fix * Introduce `unsafe-allow-redirect` * Support navigate-to directive fixes #190 fixes #193 --- salvation.iml | 18 ----- .../shapesecurity/salvation/Constants.java | 2 +- .../com/shapesecurity/salvation/Parser.java | 12 +++- .../shapesecurity/salvation/Tokeniser.java | 1 + .../shapesecurity/salvation/data/Policy.java | 55 +++++++++------ .../directiveValues/KeywordSource.java | 1 + .../salvation/directives/Directive.java | 1 + .../directives/NavigateToDirective.java | 19 ++++++ .../salvation/tokens/DirectiveNameToken.java | 3 + .../shapesecurity/salvation/LocationTest.java | 4 +- .../shapesecurity/salvation/ParserTest.java | 14 ++++ .../salvation/PolicyMergeTest.java | 67 +++++++++++++++++++ .../salvation/PolicyQueryingTest.java | 47 +++++++++++++ 13 files changed, 200 insertions(+), 44 deletions(-) delete mode 100644 salvation.iml create mode 100644 src/main/java/com/shapesecurity/salvation/directives/NavigateToDirective.java diff --git a/salvation.iml b/salvation.iml deleted file mode 100644 index 3933a03e..00000000 --- a/salvation.iml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ 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 f78e8a10..7996d935 100644 --- a/src/main/java/com/shapesecurity/salvation/Constants.java +++ b/src/main/java/com/shapesecurity/salvation/Constants.java @@ -16,7 +16,7 @@ .compile("^(?:script|style)$", Pattern.CASE_INSENSITIVE); // RFC 2045 appendix A: productions of type and subtype public static final Pattern mediaTypePattern = Pattern.compile("^(?[a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)/(?[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 accd475e..00b438b0 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.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 @@ private boolean eat(@Nonnull Class c) { 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 @@ private void enforceMissingDirectiveValue(@Nonnull Token directiveNameToken) thr 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 @@ private void enforceMissingDirectiveValue(@Nonnull Token directiveNameToken) thr 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 6663bbd9..524fe2f9 100644 --- a/src/main/java/com/shapesecurity/salvation/Tokeniser.java +++ b/src/main/java/com/shapesecurity/salvation/Tokeniser.java @@ -136,6 +136,7 @@ private boolean hasNext() { 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 d5d8751f..37a54254 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.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 boolean allowsManifestFromSource(@Nonnull GUID source) { } 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, Directive> 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 42b5d5b3..a0f17523 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 90939f5b..e656ecee 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 @@ static void register(Class directiveClass) { 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 00000000..49d77682 --- /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 sourceExpressions) { + super(NavigateToDirective.name, sourceExpressions); + } + + @Nonnull @Override public Directive construct(Set 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 12e35ca3..91f46f1b 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 enum DirectiveNameSubtype { 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 enum DirectiveNameSubtype { return ManifestSrc; case "media-src": return MediaSrc; + case "navigate-to": + return NavigateTo; case "object-src": return ObjectSrc; case "plugin-types": diff --git a/src/test/java/com/shapesecurity/salvation/LocationTest.java b/src/test/java/com/shapesecurity/salvation/LocationTest.java index 33fd5d44..47e6bbd1 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 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 00d67f53..f70ff035 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 f6a94537..e03c12d7 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 3a8fb259..85828d06 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"))); } }