Bug 446344 - Implement Origin header CSRF mitigation. r?dragana,r?ckerschb draft
authorFrancois Marier <francois@mozilla.com>
Fri, 24 Nov 2017 17:35:05 -0800
changeset 706978 b4fc7aa1aa746423c23cad1051075c4cfa7b66d3
parent 705309 91fc3a79606bdc7fb43fef12eff7e65b5b84c00e
child 742817 f286bf8d4f3c21e75b55339eea1df7b995a5e327
push id91978
push userfmarier@mozilla.com
push dateMon, 04 Dec 2017 16:13:44 +0000
reviewersdragana, ckerschb
bugs446344
milestone59.0a1
Bug 446344 - Implement Origin header CSRF mitigation. r?dragana,r?ckerschb MozReview-Commit-ID: EZpGo0UfmUP
modules/libpref/init/all.js
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/nsHttpAtomList.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/test/mochitests/mochitest.ini
netwerk/test/mochitests/origin_header.sjs
netwerk/test/mochitests/origin_header_form_post.html
netwerk/test/mochitests/origin_header_form_post_xorigin.html
netwerk/test/mochitests/test_origin_header.html
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1675,16 +1675,20 @@ pref("network.http.referer.spoofSource",
 pref("network.http.referer.hideOnionSource", false);
 // 0=full URI, 1=scheme+host+port+path, 2=scheme+host+port
 pref("network.http.referer.trimmingPolicy", 0);
 // 0=full URI, 1=scheme+host+port+path, 2=scheme+host+port
 pref("network.http.referer.XOriginTrimmingPolicy", 0);
 // 0=always send, 1=send iff base domains match, 2=send iff hosts match
 pref("network.http.referer.XOriginPolicy", 0);
 
+// Include an origin header on non-GET and non-HEAD requests regardless of CORS
+// 0=never send, 1=send when same-origin only, 2=always send
+pref("network.http.sendOriginHeader", 0);
+
 // Maximum number of consecutive redirects before aborting.
 pref("network.http.redirection-limit", 20);
 
 // Enable http compression: comment this out in case of problems with 1.1
 // NOTE: support for "compress" has been disabled per bug 196406.
 // NOTE: separate values with comma+space (", "): see bug 576033
 pref("network.http.accept-encoding", "gzip, deflate");
 pref("network.http.accept-encoding.secure", "gzip, deflate, br");
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -1682,32 +1682,20 @@ HttpBaseChannel::SetReferrerWithPolicy(n
     // Replace |referrer| with a URI without wyciwyg://123/.
     rv = NS_NewURI(getter_AddRefs(referrerGrip),
                    Substring(path, slashIndex + 1, pathLength - slashIndex - 1));
     if (NS_FAILED(rv)) return rv;
 
     referrer = referrerGrip.get();
   }
 
-  //
-  // block referrer if not on our white list...
-  //
-  static const char *const referrerWhiteList[] = {
-    "http",
-    "https",
-    "ftp",
-    nullptr
-  };
-  match = false;
-  const char *const *scheme = referrerWhiteList;
-  for (; *scheme && !match; ++scheme) {
-    rv = referrer->SchemeIs(*scheme, &match);
-    if (NS_FAILED(rv)) return rv;
+  // Enforce Referrer whitelist
+  if (!IsReferrerSchemeAllowed(referrer)) {
+    return NS_OK; // kick out....
   }
-  if (!match) return NS_OK; // kick out....
 
   //
   // Handle secure referrals.
   //
   // Support referrals from a secure server if this is a secure site
   // and (optionally) if the host names are the same.
   //
   rv = referrer->SchemeIs("https", &match);
@@ -3317,16 +3305,34 @@ HttpBaseChannel::AddCookiesToRequest()
     cookie = mUserSetCookieHeader;
   }
 
   // If we are in the child process, we want the parent seeing any
   // cookie headers that might have been set by SetRequestHeader()
   SetRequestHeader(nsDependentCString(nsHttp::Cookie), cookie, false);
 }
 
+/* static */
+bool
+HttpBaseChannel::IsReferrerSchemeAllowed(nsIURI *aReferrer)
+{
+  NS_ENSURE_TRUE(aReferrer, false);
+
+  nsAutoCString scheme;
+  nsresult rv = aReferrer->GetScheme(scheme);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  if (scheme.EqualsIgnoreCase("https") ||
+      scheme.EqualsIgnoreCase("http") ||
+      scheme.EqualsIgnoreCase("ftp")) {
+    return true;
+  }
+  return false;
+}
+
 bool
 HttpBaseChannel::ShouldRewriteRedirectToGET(uint32_t httpStatus,
                                             nsHttpRequestHead::ParsedMethodType method)
 {
   // for 301 and 302, only rewrite POST
   if (httpStatus == 301 || httpStatus == 302)
     return method == nsHttpRequestHead::kMethod_Post;
 
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -343,16 +343,18 @@ public:
     const NetAddr& GetPeerAddr() { return mPeerAddr; }
 
     MOZ_MUST_USE nsresult OverrideSecurityInfo(nsISupports* aSecurityInfo);
 
 public: /* Necko internal use only... */
     int64_t GetAltDataLength() { return mAltDataLength; }
     bool IsNavigation();
 
+    static bool IsReferrerSchemeAllowed(nsIURI *aReferrer);
+
     // Return whether upon a redirect code of httpStatus for method, the
     // request method should be rewritten to GET.
     static bool ShouldRewriteRedirectToGET(uint32_t httpStatus,
                                            nsHttpRequestHead::ParsedMethodType method);
 
     // Like nsIEncodedChannel::DoApplyConversions except context is set to
     // mListenerContext.
     MOZ_MUST_USE nsresult
--- a/netwerk/protocol/http/nsHttpAtomList.h
+++ b/netwerk/protocol/http/nsHttpAtomList.h
@@ -58,16 +58,17 @@ HTTP_ATOM(If_None_Match_Any,         "If
 HTTP_ATOM(If_Range,                  "If-Range")
 HTTP_ATOM(If_Unmodified_Since,       "If-Unmodified-Since")
 HTTP_ATOM(Keep_Alive,                "Keep-Alive")
 HTTP_ATOM(Last_Modified,             "Last-Modified")
 HTTP_ATOM(Lock_Token,                "Lock-Token")
 HTTP_ATOM(Link,                      "Link")
 HTTP_ATOM(Location,                  "Location")
 HTTP_ATOM(Max_Forwards,              "Max-Forwards")
+HTTP_ATOM(Origin,                    "Origin")
 HTTP_ATOM(Overwrite,                 "Overwrite")
 HTTP_ATOM(Pragma,                    "Pragma")
 HTTP_ATOM(Prefer,                    "Prefer")
 HTTP_ATOM(Proxy_Authenticate,        "Proxy-Authenticate")
 HTTP_ATOM(Proxy_Authorization,       "Proxy-Authorization")
 HTTP_ATOM(Proxy_Connection,          "Proxy-Connection")
 HTTP_ATOM(Range,                     "Range")
 HTTP_ATOM(Referer,                   "Referer")
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -6210,16 +6210,17 @@ nsHttpChannel::BeginConnect()
 
     nsCOMPtr<nsProxyInfo> proxyInfo;
     if (mProxyInfo)
         proxyInfo = do_QueryInterface(mProxyInfo);
 
     mRequestHead.SetHTTPS(isHttps);
     mRequestHead.SetOrigin(scheme, host, port);
 
+    SetOriginHeader();
     SetDoNotTrack();
 
     OriginAttributes originAttributes;
     NS_GetOriginAttributes(this, originAttributes);
 
     RefPtr<AltSvcMapping> mapping;
     if (!mConnectionInfo && mAllowAltSvc && // per channel
         !(mLoadFlags & LOAD_FRESH_CONNECTION) &&
@@ -8949,16 +8950,68 @@ nsHttpChannel::SetLoadGroupUserAgentOver
                 SetRequestHeader(NS_LITERAL_CSTRING("User-Agent"), ua, false);
             } else {
                 gHttpHandler->OnUserAgentRequest(this);
             }
         }
     }
 }
 
+// Step 10 of HTTP-network-or-cache fetch
+void
+nsHttpChannel::SetOriginHeader()
+{
+    if (mRequestHead.IsGet() || mRequestHead.IsHead()) {
+        return;
+    }
+    nsAutoCString existingHeader;
+    Unused << mRequestHead.GetHeader(nsHttp::Origin, existingHeader);
+    if (!existingHeader.IsEmpty()) {
+        LOG(("nsHttpChannel::SetOriginHeader Origin header already present"));
+        return;
+    }
+
+    DebugOnly<nsresult> rv;
+
+    // Instead of consulting Preferences::GetInt() all the time we
+    // can cache the result to speed things up.
+    static int32_t sSendOriginHeader = 0;
+    static bool sIsInited = false;
+    if (!sIsInited) {
+        sIsInited = true;
+        Preferences::AddIntVarCache(&sSendOriginHeader,
+                                    "network.http.sendOriginHeader");
+    }
+    if (sSendOriginHeader == 0) {
+        // Origin header suppressed by user setting
+        return;
+    }
+
+    nsCOMPtr<nsIURI> referrer;
+    mLoadInfo->TriggeringPrincipal()->GetURI(getter_AddRefs(referrer));
+
+    nsAutoCString origin("null");
+    if (referrer && IsReferrerSchemeAllowed(referrer)) {
+        nsContentUtils::GetASCIIOrigin(referrer, origin);
+    }
+
+    // Restrict Origin to same-origin loads if requested by user
+    if (sSendOriginHeader == 1) {
+        nsAutoCString currentOrigin;
+        nsContentUtils::GetASCIIOrigin(mURI, currentOrigin);
+        if (!origin.EqualsIgnoreCase(currentOrigin.get())) {
+            // Origin header suppressed by user setting
+            return;
+        }
+    }
+
+    rv = mRequestHead.SetHeader(nsHttp::Origin, origin, false /* merge */);
+    MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
 void
 nsHttpChannel::SetDoNotTrack()
 {
   /**
    * 'DoNotTrack' header should be added if 'privacy.donottrackheader.enabled'
    * is true or tracking protection is enabled. See bug 1258033.
    */
   nsCOMPtr<nsILoadContext> loadContext;
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -485,16 +485,17 @@ private:
                                                bool checkingAppCacheEntry);
 
     void SetPushedStream(Http2PushedStream *stream);
 
     void MaybeWarnAboutAppCache();
 
     void SetLoadGroupUserAgentOverride();
 
+    void SetOriginHeader();
     void SetDoNotTrack();
 
     already_AddRefed<nsChannelClassifier> GetOrCreateChannelClassifier();
 
     // Start an internal redirect to a new InterceptedHttpChannel which will
     // resolve in firing a ServiceWorker FetchEvent.
     MOZ_MUST_USE nsresult RedirectToInterceptedChannel();
 
--- a/netwerk/test/mochitests/mochitest.ini
+++ b/netwerk/test/mochitests/mochitest.ini
@@ -10,16 +10,19 @@ support-files =
   file_loadinfo_redirectchain.sjs
   file_1331680.js
   file_iframe_allow_scripts.html
   file_iframe_allow_same_origin.html
   redirect_idn.html^headers^
   redirect_idn.html
   empty.html
   redirect.sjs
+  origin_header.sjs
+  origin_header_form_post.html
+  origin_header_form_post_xorigin.html
 
 [test_arraybufferinputstream.html]
 [test_idn_redirect.html]
 [test_loadinfo_redirectchain.html]
 [test_partially_cached_content.html]
 [test_rel_preconnect.html]
 [test_redirect_ref.html]
 [test_uri_scheme.html]
@@ -27,8 +30,9 @@ support-files =
 [test_user_agent_updates.html]
 [test_user_agent_updates_reset.html]
 [test_viewsource_unlinkable.html]
 [test_xhr_method_case.html]
 [test_1331680.html]
 [test_1331680_iframe.html]
 [test_1331680_xhr.html]
 [test_1396395.html]
+[test_origin_header.html]
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/origin_header.sjs
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+    response.setHeader("Content-Type", "text/plain", false);
+    response.setHeader("Cache-Control", "no-cache", false);
+
+    var origin = request.hasHeader("Origin") ? request.getHeader("Origin") : "";
+    response.write("Origin: " + origin);
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/origin_header_form_post.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+    <script>
+     function submitForm() {
+         document.getElementById("form").submit();
+     }
+    </script>
+</head>
+<body onload="submitForm()">
+    <form action="http://Mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs"
+          method="POST"
+          id="form">
+        <input type="submit" value="Submit POST">
+    </form>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/origin_header_form_post_xorigin.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+    <script>
+     function submitForm() {
+         document.getElementById("form").submit();
+     }
+    </script>
+</head>
+<body onload="submitForm()">
+    <form action="http://test1.mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs"
+          method="POST"
+          id="form">
+        <input type="submit" value="Submit POST">
+    </form>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/test_origin_header.html
@@ -0,0 +1,341 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+    <title> Bug 446344 - Test Origin Header</title>
+    <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446344">Mozilla Bug 446344</a></p>
+
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const EMPTY_ORIGIN = "Origin: ";
+
+let testsToRun = [
+    {
+        name: "sendOriginHeader=0 (never)",
+        prefs: [
+            ["network.http.sendOriginHeader", 0],
+        ],
+        results: {
+            framePost: EMPTY_ORIGIN,
+            framePostXOrigin: EMPTY_ORIGIN,
+            frameGet: EMPTY_ORIGIN,
+            framePostNonSandboxed: EMPTY_ORIGIN,
+            framePostNonSandboxedXOrigin: EMPTY_ORIGIN,
+            framePostSandboxed: EMPTY_ORIGIN,
+            framePostSrcDoc: EMPTY_ORIGIN,
+            framePostSrcDocXOrigin: EMPTY_ORIGIN,
+            framePostDataURI: EMPTY_ORIGIN,
+        },
+    },
+    {
+        name: "sendOriginHeader=1 (same-origin)",
+        prefs: [
+            ["network.http.sendOriginHeader", 1],
+        ],
+        results: {
+            framePost: "Origin: http://mochi.test:8888",
+            framePostXOrigin: EMPTY_ORIGIN,
+            frameGet: EMPTY_ORIGIN,
+            framePostNonSandboxed: "Origin: http://mochi.test:8888",
+            framePostNonSandboxedXOrigin: EMPTY_ORIGIN,
+            framePostSandboxed: EMPTY_ORIGIN,
+            framePostSrcDoc: "Origin: http://mochi.test:8888",
+            framePostSrcDocXOrigin: EMPTY_ORIGIN,
+            framePostDataURI: EMPTY_ORIGIN,
+        },
+    },
+    {
+        name: "sendOriginHeader=2 (always)",
+        prefs: [
+            ["network.http.sendOriginHeader", 2],
+        ],
+        results: {
+            framePost: "Origin: http://mochi.test:8888",
+            framePostXOrigin: "Origin: http://mochi.test:8888",
+            frameGet: EMPTY_ORIGIN,
+            framePostNonSandboxed: "Origin: http://mochi.test:8888",
+            framePostNonSandboxedXOrigin: "Origin: http://mochi.test:8888",
+            framePostSandboxed: "Origin: null",
+            framePostSrcDoc: "Origin: http://mochi.test:8888",
+            framePostSrcDocXOrigin: "Origin: http://mochi.test:8888",
+            framePostDataURI: "Origin: null",
+        },
+    },
+    {
+        name: "sendRefererHeader=0 (never)",
+        prefs: [
+            ["network.http.sendRefererHeader", 0],
+        ],
+        results: {
+            framePost: "Origin: http://mochi.test:8888",
+            framePostXOrigin: "Origin: http://mochi.test:8888",
+            frameGet: EMPTY_ORIGIN,
+            framePostNonSandboxed: "Origin: http://mochi.test:8888",
+            framePostNonSandboxedXOrigin: "Origin: http://mochi.test:8888",
+            framePostSandboxed: "Origin: null",
+            framePostSrcDoc: "Origin: http://mochi.test:8888",
+            framePostSrcDocXOrigin: "Origin: http://mochi.test:8888",
+            framePostDataURI: "Origin: null",
+        },
+    },
+    {
+        name: "userControlPolicy=0 (no-referrer)",
+        prefs: [
+            ["network.http.sendRefererHeader", 2],
+            ["network.http.referer.userControlPolicy", 0],
+        ],
+        results: {
+            framePost: "Origin: http://mochi.test:8888",
+            framePostXOrigin: "Origin: http://mochi.test:8888",
+            frameGet: EMPTY_ORIGIN,
+            framePostNonSandboxed: "Origin: http://mochi.test:8888",
+            framePostNonSandboxedXOrigin: "Origin: http://mochi.test:8888",
+            framePostSandboxed: "Origin: null",
+            framePostSrcDoc: "Origin: http://mochi.test:8888",
+            framePostSrcDocXOrigin: "Origin: http://mochi.test:8888",
+            framePostDataURI: "Origin: null",
+        },
+    },
+];
+
+let checksToRun = [
+    {
+        name: "POST",
+        frameID: "framePost",
+        formID: "formPost",
+    },
+    {
+        name: "cross-origin POST",
+        frameID: "framePostXOrigin",
+        formID: "formPostXOrigin",
+    },
+    {
+        name: "GET",
+        frameID: "frameGet",
+        formID: "formGet",
+    },
+    {
+        name: "POST inside iframe",
+        frameID: "framePostNonSandboxed",
+        frameSrc: "HTTP://mochi.test:8888/tests/netwerk/test/mochitests/origin_header_form_post.html",
+    },
+    {
+        name: "cross-origin POST inside iframe",
+        frameID: "framePostNonSandboxedXOrigin",
+        frameSrc: "Http://mochi.test:8888/tests/netwerk/test/mochitests/origin_header_form_post_xorigin.html",
+    },
+    {
+        name: "POST inside sandboxed iframe",
+        frameID: "framePostSandboxed",
+        frameSrc: "http://mochi.test:8888/tests/netwerk/test/mochitests/origin_header_form_post.html",
+    },
+    {
+        name: "POST inside a srcdoc iframe",
+        frameID: "framePostSrcDoc",
+        srcdoc: "origin_header_form_post.html",
+    },
+    {
+        name: "cross-origin POST inside a srcdoc iframe",
+        frameID: "framePostSrcDocXOrigin",
+        srcdoc: "origin_header_form_post_xorigin.html",
+    },
+    {
+        name: "POST inside a data: iframe",
+        frameID: "framePostDataURI",
+        dataURI: "origin_header_form_post.html",
+    },
+];
+
+function frameLoaded(test, check)
+{
+    let frame = window.document.getElementById(check.frameID);
+    frame.onload = null;
+    let result = SpecialPowers.wrap(frame).contentDocument.documentElement.textContent;
+    is(result, test.results[check.frameID], check.name + " with " + test.name);
+}
+
+function submitForm(test, check)
+{
+     return new Promise((resolve, reject) => {
+         document.getElementById(check.frameID).onload = () => {
+             frameLoaded(test, check);
+             resolve();
+         };
+         document.getElementById(check.formID).submit();
+     });
+}
+
+function loadIframe(test, check)
+{
+    return new Promise((resolve, reject) => {
+        let frame = SpecialPowers.wrap(window.document.getElementById(check.frameID));
+        frame.onload = function () {
+            // Ignore the first load and wait for the submitted form instead.
+            let location = frame.contentWindow.location + "";
+            if (location.endsWith("origin_header.sjs")) {
+                frameLoaded(test, check);
+                resolve();
+            }
+        }
+        frame.src = check.frameSrc;
+    });
+}
+
+function loadSrcDocFrame(test, check)
+{
+    return new Promise((resolve, reject) => {
+        let frame = SpecialPowers.wrap(window.document.getElementById(check.frameID));
+        frame.onload = function () {
+            // Ignore the first load and wait for the submitted form instead.
+            let location = frame.contentWindow.location + "";
+            if (location.endsWith("origin_header.sjs")) {
+                frameLoaded(test, check);
+                resolve();
+            }
+        }
+        fetch(check.srcdoc).then((response) => {
+            response.text().then((body) => {
+                frame.srcdoc = body;
+            });;
+        });
+    });
+ }
+
+function loadDataURIFrame(test, check)
+{
+    return new Promise((resolve, reject) => {
+        let frame = SpecialPowers.wrap(window.document.getElementById(check.frameID));
+        frame.onload = function () {
+            // Ignore the first load and wait for the submitted form instead.
+            let location = frame.contentWindow.location + "";
+            if (location.endsWith("origin_header.sjs")) {
+                frameLoaded(test, check);
+                resolve();
+            }
+        }
+        fetch(check.dataURI).then((response) => {
+            response.text().then((body) => {
+                frame.src = "data:text/html," + encodeURIComponent(body);
+            });;
+        });
+    });
+}
+
+async function resetFrames()
+{
+    let checkPromises = [];
+    for (check of checksToRun) {
+        checkPromises.push(new Promise((resolve, reject) => {
+            let frame = document.getElementById(check.frameID);
+            frame.onload = () => resolve();
+            if (check.srcdoc) {
+                frame.srcdoc = "";
+            } else {
+                frame.src = "about:blank";
+            }
+        }));
+    }
+    await Promise.all(checkPromises);
+}
+
+async function runTests()
+{
+    for (test of testsToRun) {
+        await resetFrames();
+        await SpecialPowers.pushPrefEnv({"set": test.prefs});
+
+        let checkPromises = [];
+        for (check of checksToRun) {
+            if (check.formID) {
+                checkPromises.push(submitForm(test, check));
+            } else if (check.frameSrc) {
+                checkPromises.push(loadIframe(test, check));
+            } else if (check.srcdoc) {
+                checkPromises.push(loadSrcDocFrame(test, check));
+            } else if (check.dataURI) {
+                checkPromises.push(loadDataURIFrame(test, check));
+            } else {
+                ok(false, "Unsupported check");
+                break;
+            }
+        }
+        await Promise.all(checkPromises);
+    };
+    SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(5); // work around Android timeouts
+addLoadEvent(runTests);
+
+</script>
+</pre>
+<table>
+<tr>
+    <td>
+        <iframe src="about:blank" name="framePost" id="framePost"></iframe>
+        <form action="origin_header.sjs"
+              method="POST"
+              id="formPost"
+              target="framePost">
+            <input type="submit" value="Submit POST">
+        </form>
+    </td>
+    <td>
+        <iframe src="about:blank" name="framePostXOrigin" id="framePostXOrigin"></iframe>
+        <form action="http://test1.mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs"
+              method="POST"
+              id="formPostXOrigin"
+              target="framePostXOrigin">
+            <input type="submit" value="Submit XOrigin POST">
+        </form>
+    </td>
+    <td>
+        <iframe src="about:blank" name="frameGet" id="frameGet"></iframe>
+        <form action="origin_header.sjs"
+              method="GET"
+              id="formGet"
+              target="frameGet">
+            <input type="submit" value="Submit GET">
+        </form>
+    </td>
+</tr>
+<tr>
+    <td>
+        <iframe src="about:blank" id="framePostNonSandboxed"></iframe>
+        <div>Non-sandboxed iframe</div>
+    </td>
+    <td>
+        <iframe src="about:blank" id="framePostNonSandboxedXOrigin"></iframe>
+        <div>Non-sandboxed cross-origin iframe</div>
+    </td>
+    <td>
+        <iframe src="about:blank" id="framePostSandboxed" sandbox="allow-forms allow-scripts"></iframe>
+        <div>Sandboxed iframe</div>
+    </td>
+</tr>
+<tr>
+    <td>
+        <iframe id="framePostSrcDoc" src="about:blank"></iframe>
+        <div>Srcdoc iframe</div>
+    </td>
+    <td>
+        <iframe id="framePostSrcDocXOrigin" src="about:blank"></iframe>
+        <div>Srcdoc cross-origin iframe</div>
+    </td>
+    <td>
+        <iframe id="framePostDataURI" src="about:blank"></iframe>
+        <div>data: URI iframe</div>
+    </td>
+</tr>
+</table>
+
+</body>
+</html>