Skip to content

Instantly share code, notes, and snippets.

@shuhei
Last active February 12, 2020 22:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shuhei/c5e8ea9436287ed194eb61d2a92223a3 to your computer and use it in GitHub Desktop.
Save shuhei/c5e8ea9436287ed194eb61d2a92223a3 to your computer and use it in GitHub Desktop.
Firefox http2 beacon bug

Create a key pair.

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
  -keyout localhost-privkey.pem -out localhost-cert.pem

Run the server.

node index.js

Open https://localhost:8443/ with Firefox.

const http2 = require("http2");
const { HTTP2_HEADER_PATH } = http2.constants;
const fs = require("fs");
const SCRIPT = `
function fetchAndSendBeacon(button, beaconPath) {
fetch("/slow").then(() => {
button.textContent += ": done";
}, () => {
button.textContent += ": error";
});
setTimeout(() => {
navigator.sendBeacon(beaconPath, "{}");
}, 0);
}
document.getElementById("without-body").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-without-body");
});
document.getElementById("with-body").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-with-body");
});
document.getElementById("with-body-and-delay").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-with-body-and-delay");
});
document.getElementById("with-double-body").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-with-double-body");
});
`;
const PAGE = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>
<button id="without-body">Without Body</button>
<button id="with-body">With Body</button>
<button id="with-body-and-delay">With Body and Delay</button>
<button id="with-double-body">With Double Body</button>
<script>${SCRIPT}</script>
</body>
</html>
`;
function log(...args) {
console.log(`\u001b[32m[${new Date().toISOString()}]\u001b[0m`, ...args);
}
const server = http2.createSecureServer({
key: fs.readFileSync("localhost-privkey.pem"),
cert: fs.readFileSync("localhost-cert.pem")
});
server.on("error", err => {
log("server error", err);
});
server.on("session", session => {
log("session created");
session.on("error", err => {
log("session error:", err);
});
session.on("stream", (stream, headers) => {
stream.on("error", err => {
log("stream error:", err);
});
switch (headers[HTTP2_HEADER_PATH]) {
case "/":
stream.respond({
"content-type": "text/html",
":status": 200
});
stream.end(PAGE);
break;
case "/beacon-without-body":
stream.respond({
"content-type": "text/plain",
":status": 200
});
stream.end();
break;
case "/beacon-with-body": {
const body = "Hello World";
stream.respond({
"content-type": "text/plain",
"content-length": body.length,
":status": 200
});
stream.end(body);
break;
}
case "/beacon-with-body-and-delay": {
const body = "Hello World";
stream.respond({
"content-type": "text/plain",
"content-length": body.length,
":status": 200
});
stream.write(body);
// Firefox sends GOAWAY if this delay exists.
setTimeout(() => {
stream.end();
}, 10);
break;
}
case "/beacon-with-double-body": {
const body = "Hello World";
stream.respond({
"content-type": "text/plain",
"content-length": body.length * 2,
":status": 200
});
stream.write(body);
setTimeout(() => {
if (!stream.closed) {
stream.end(body);
}
}, 10);
break;
}
case "/slow":
setTimeout(() => {
if (!stream.closed) {
stream.respond({
"content-type": "text/plain",
":status": 200
});
stream.end("Hello World");
}
}, 500);
break;
default:
stream.respond({
"content-type": "text/html",
":status": 401
});
stream.end("<h1>Not found</h1>");
}
});
});
server.listen(8443);
@shuhei
Copy link
Author

shuhei commented Feb 11, 2020

Also, if it doesn't send goaway if the content-length is not set in the response body.

@shuhei
Copy link
Author

shuhei commented Feb 12, 2020

Code that can set NS_ERROR_NET_INTERRUPT (from grep):

dom/base/Navigator.cpp:1050:  aRequest->Cancel(NS_ERROR_NET_INTERRUPT);`
netwerk/base/nsSocketTransport2.cpp:155:      rv = NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/Http2Session.cpp:3390:                              : NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/Http3Session.cpp:671:      CloseStream(stream, NS_ERROR_NET_INTERRUPT);
netwerk/protocol/http/nsHttpChannel.cpp:8372:          aStatus = NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/nsHttpTransaction.cpp:1631:      return NS_ERROR_NET_INTERRUPT;

Not relevant:

  • netwerk/protocol/http/Http2Session.cpp:3390: This should be called after RST_STREAM, but RST_STREAM is not captured in Wireshark when GOAWAY happens.
  • netwerk/protocol/http/Http3Session.cpp:671: This issue is about HTTP2 instead of HTTP3.
  • netwerk/protocol/http/nsHttpChannel.cpp:8372: This line is about Range request failure, but the beacon is not using Range header.

Relevant?:

  • dom/base/Navigator.cpp:1050: The initial ticket for navigator.sendBeacon has some discussions around it. https://bugzilla.mozilla.org/show_bug.cgi?id=936340
  • netwerk/base/nsSocketTransport2.cpp:155: PR_END_OF_FILE_ERROR from NSPR (Netscape Portable Runtime) is translated to NS_ERROR_NET_INTERRUPT. It seems that PR_END_OF_FILE_ERROR can come from TLS-related low-level functions. This may not be relevant.
  • netwerk/protocol/http/nsHttpTransaction.cpp:1631: A comment suggests that it's for 100 class response, but not sure.

@shuhei
Copy link
Author

shuhei commented Feb 12, 2020

  • 73: I can reproduce.
  • 74.0b2 (Beta): I can reproduce.
  • 75.0a1 (Nightly): I can't reproduce.

@shuhei
Copy link
Author

shuhei commented Feb 12, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment