Closed Bug 1911942 Opened 1 year ago Closed 1 year ago

Dangerously permissive Content-Type header value parsing

Categories

(Core :: Networking: HTTP, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 1895075

People

(Reporter: mateusz.krzeszowiec, Unassigned)

References

(Depends on 1 open bug)

Details

(Keywords: reporter-external, Whiteboard: [client-bounty-form])

Attachments

(1 file)

1.23 KB, application/x-javascript
Details
Attached file index.js

VULNERABILITY DETAILS

Firefox accepts malformed Content-Type "executable" mime types (ones that result in an HTML document being rendered with JavaScript that will be executed), which may facilitate Content-Type validation bypasses.

In most severe cases, this leads to easier exploitation of cross-site scripting vulnerabilities in web applications. I've tested and verified unexpected behaviour for the "text/html" content type only, but it is very likely that it's a more generic bug in content-type parsing that needs to be addressed.

VERSION

Current Firefox version on macos

REPRODUCTION CASE

It's easiest to reproduce when running one's handwritten web server or using an intercepting proxy.

Using a trivial HTTP server like the one below, written in Node.js (attached):

const { createServer } = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;

const server = createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html'); // (1)
res.end(<html><head><title>OMG?</title></head><body><a href="javascript: alert('omg');">test</a></body></html>);
});

server.listen(port, hostname, () => {
console.log(Server running at http://${hostname}:${port}/);
});
Add a non-permitted character (like ";" delimiter, as defined here: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6) at the beggining of a content-type value on line (1), modifying it into, for example:

res.setHeader('Content-Type', ';text/html');
another variant of this issue can look as follows:

res.setHeader('Content-Type', 'text / <html');
or the delimiter can be appended to type as follows:

res.setHeader('Content-Type', 'text;/html');
Run the server by executing, for example: npx nodemon index.js

Now visit the page by pointing Firefox to, for example: http://localhost:3000

The document will be rendered as HTML which is unexpected and invalid behavior.

The key is that the webpage will be rendered as HTML document whether the content-type is set to an "executable" content-type like "text/html" but will also be rendered as a regular HTML document if the content-type is set to different

CREDIT INFORMATION

Reporter credit: Mateusz Krzeszowiec, inspired by @a_zara_n @ei01241 from Flatt Security

Flags: sec-bounty?

I'm not familiar with this code, but it looks like maybe this parsing is happening in nsMultiMixedConv::OnStartRequest, so I'll move this to Networking.

Group: firefox-core-security → network-core-security
Component: Security → Networking
Product: Firefox → Core

Found another odd variant again accepted as a "proper" text/html content type by FF:

res.setHeader('Content-Type', 't;ext/html');

The severity field is not set for this bug.
:jesup, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(rjesup)

Unfortunately, browsers have been "dangerously permission" out of historical necessity since the 90s. More relevant to browsers than the RFC is the WHATWG Mime-sniffing spec: https://mimesniff.spec.whatwg.org/ It's possible we're not following that correctly, too, but let's start there.

Simon: are we getting this wrong?

Most important (and the reason for the WHATWG specs) is that we behave the same as other browsers so that Firefox users are not uniquely vulnerable in the face of an insecure/abused server.

Flags: needinfo?(zcorpan)

Tests at https://wpt.fyi/results/mimesniff/mime-types?label=master&label=experimental&aligned

It looks like roughly the same tests pass and fail in Firefox as in Chrome and Safari.

Flags: needinfo?(zcorpan)

I understand that it is how it's working at the moment but I'm seeing a contrast between RFC/whatwg and actual implementation in browsers.
What also struck me is that if X-Content-Type-Options: nosniff is present, Firefox will fall back to text/plain content type in some cases or bring up a download dialogue in other cases.

This does not work as described here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options - current behaviour goes as follows:
If the nosniff is set, then

  • the document will be rendered with the provided content-type, if the content type is NOT malformed
  • firefox will fallback to text/plain in some cases (for example: "text/sd", "firefoxisthebest")
  • but not the others (for example: "boom!boom", "textz/sd")

And to be specific where FF deviates from the algorithm described in whatwg:

  • https://mimesniff.spec.whatwg.org/#parsing-a-mime-type
  • point 7 says: "Let subtype be the result of collecting a sequence of code points that are not U+003B (;) from input, given position."
  • the actual behaviour is that FF seem to ignore white spaces, so the "text/html <" is still considered valid "text/html" content-type (that's with nosniff set)

I guess that if this is "works-as-expected" then please close this ticket. I'll try to document it somewhere if this is the case.

The problem is not what the spec says; the problem is what is required to actually make websites work. We'd probably first have to roll out some kind of data gathering to figure out how frequently we rely on non-compliant content types before we could be stricter. Right now I don't think we know one way or the other.

It's getting weirder; you can force FF (and Chrome) to pick the SECOND mime-type in a Content-Type header value - if you add a comma.
This weirdly-looking binary string is a GIF with a comment that contains HTML.

TLDR:

  • Given "Content-Type: image/gif text/html" FF will render document as image/gif, (again with X-Content-Type-Options: nosniff)
  • Given "Content-Type: image/gif, text/html" FF will render document as text/html, (that's with X-Content-Type-Options: nosniff)
const url = require('node:url');

const hostname = 'localhost';
const port = 3000;

const server = createServer((req, res) => {

    const parsedUrl = url.parse(req.url, true);
    const gif = Buffer.from('47494638396130003000a10100000000ffffffffffffffffff21fe2f3c7374726f6e673e436f6e74656e742d547970652070617273696e6720697320626f726b65643c2f7374726f6e673e0021f904010a0002002c00000000300030000002c98c8f99c01da0a29cb1d9eba89eb8e30d1adef8851c8966e692b6d07ab832fcc82e6dcf66ae877cbff9dd40c2215118f3f87895cece26512e5b1ae714320a968eafd863b5d57669490b787c427b6bea6ad5fc7e77dba2cb514e2f33f4d13cbfa9125626a6562288a042015717b8b887989836076798b888876567c738b6c945c5f6f87745323ae9079833c806c3c4b97a36e4c9da2a1b79490ac542f603c8abea8ba3ab809b8bd2872a99929a8cbccc526c9ca51719fb1c5a5d7b7df87bcc294a0639cd185e6e2e5100003b', 'hex');

    switch (parsedUrl.query.case) {
            case 'gif':
                res.setHeader('X-Content-Type-Options', 'nosniff');
                res.setHeader('Content-Type', 'image/gif text/html');
                res.end(gif);
                break;

            case 'look-ma-no-hands':
                res.setHeader('X-Content-Type-Options', 'nosniff');
                res.setHeader('Content-Type', 'image/gif, text/html');
                res.end(gif);
                break;
    }
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});```

This doesn't need to be hidden

Group: network-core-security

It's getting weirder; you can force FF (and Chrome) to pick the SECOND mime-type in a Content-Type header value - if you add a comma.

If you add a comma then it is interpreted as two Content-Type: headers that were coalesced into one (a feature of HTTP). At that point browsers have to decide if they use the first, the last, ignore it completely, or in some cases (like Content-Security-Policy:) use all of them. It causes compatibility problems when browsers make different choices, so the behavior has been specified (probably in one of the browser-centric WHATWG specs).

Component: Networking → Networking: HTTP

See the pref network.standard_content_type_parsing.response_headers -- # If true, HTTP response content-type headers will be parsed using the standards-compliant MimeType parser

Note that RFC 2616 3.7 (which our non-compliant parser implements) has this: media-type = type "/" subtype *( ";" parameter ) -- that may be where the issue with ';' comes from.

This is the parsing our non-compliant parser uses (from the source https://searchfox.org/mozilla-central/source/netwerk/base/nsURLHelper.cpp#827):

 // Augmented BNF (from RFC 2616 section 3.7):
  //
  //   header-value = media-type *( LWS "," LWS media-type )
  //   media-type   = type "/" subtype *( LWS ";" LWS parameter )
  //   type         = token
  //   subtype      = token
  //   parameter    = attribute "=" value
  //   attribute    = token
  //   value        = token | quoted-string

Note that this BNF doesn't allow for "type1 type2", so "Content-Type: image/gif text/html" might take the entire string as a media type, or more likely fail to find a valid media-type. See https://searchfox.org/mozilla-central/source/netwerk/base/nsURLHelper.cpp#669 I haven't worked through all the details of parsing a Content-Type with spaces separating. (It might ignore the entire thing, and fall back to sniffing)

When parsing with a ";" in a media type (see comment 0), that will be found as the start of parameters by https://searchfox.org/mozilla-central/source/netwerk/base/nsURLHelper.cpp#723

Note: "We also want to reject a mime-type if it does not include a slash."

I presume we don't want to switch to spec-compliant parsing because it would break a bunch of websites (valentin?)

Flags: needinfo?(rjesup) → needinfo?(valentin.gosu)

https://searchfox.org/mozilla-central/rev/fe3fa7f53037d4e869858fef4ff9310dfa795c41/netwerk/base/nsURLHelper.cpp#908-911

// changed, then don't wipe-out an existing aContentCharset.  We
// also want to reject a mime-type if it does not include a slash.
// some servers give junk after the charset parameter, which may
// include a comma, so this check makes us a bit more tolerant.

This is coming from 2005 - before the initial mercurial import.
https://github.com/mozilla/gecko-dev/commit/6c59f965fd51f61711bedcd637691988984fc49d
I assume this was just to avoid breaking non-compliant webservers a long time ago.

We were moving in the direction of a more standard-compliant content type parser, but we didn't yet land bug 1895075 due to concerns regarding webcompat.

Flags: needinfo?(valentin.gosu)
See Also: → 1895075

Is this effectively a duplicate of bug 1895075? If not, what would we do in this bug that isn't in that one?

Depends on: 1895075
Flags: needinfo?(valentin.gosu)

Let's dupe. Thanks!

Status: UNCONFIRMED → RESOLVED
Closed: 1 year ago
Duplicate of bug: 1895075
Flags: needinfo?(valentin.gosu)
Resolution: --- → DUPLICATE
Flags: sec-bounty? → sec-bounty-
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: