Skip to content

Commit

Permalink
fix: properly parse relative URL with a "@" character
Browse files Browse the repository at this point in the history
A query parameter with a "@" character could be incorrectly parsed.

Example: "/foo?bar=@example.com" => host: example.com

The parse() method is also used in the `socket.io-client` package, to
extract the namespace and the query parameters.

Notes:

- this bug does not seem exploitable, as an attacker would need to
inject the query parameter in the code executed by the client.

- we might use the URL object in the next major version, but that
means dropping support for some platforms such as IE

Reference: https://caniuse.com/url

Thanks to Li Jiantao of STAR Labs (@starlabs_sg) for the responsible
disclosure.
  • Loading branch information
darrachequesne committed Jan 9, 2023
1 parent ed6d016 commit 12b7d78
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 2 deletions.
16 changes: 14 additions & 2 deletions lib/contrib/parseuri.ts
@@ -1,11 +1,23 @@
// imported from https://github.com/galkn/parseuri
/**
* Parses an URI
* Parses a URI
*
* Note: we could also have used the built-in URL object, but it isn't supported on all platforms.
*
* See:
* - https://developer.mozilla.org/en-US/docs/Web/API/URL
* - https://caniuse.com/url
* - https://www.rfc-editor.org/rfc/rfc3986#appendix-B
*
* History of the parse() method:
* - first commit: https://github.com/socketio/socket.io-client/commit/4ee1d5d94b3906a9c052b459f1a818b15f38f91c
* - export into its own module: https://github.com/socketio/engine.io-client/commit/de2c561e4564efeb78f1bdb1ba39ef81b2822cb3
* - reimport: https://github.com/socketio/engine.io-client/commit/df32277c3f6d622eec5ed09f493cae3f3391d242
*
* @author Steven Levithan <stevenlevithan.com> (MIT license)
* @api private
*/
const re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
const re = /^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;

const parts = [
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Expand Up @@ -14,6 +14,7 @@ require("./socket");
require("./transport");
require("./connection");
require("./xmlhttprequest");
require("./parseuri");

if (typeof ArrayBuffer !== "undefined") {
require("./arraybuffer");
Expand Down
68 changes: 68 additions & 0 deletions test/parseuri.js
@@ -0,0 +1,68 @@
// imported from https://github.com/galkn/parseuri
const expect = require("expect.js");
const parseuri = require("..").parse;

describe("parseuri", function () {
it("should parse an uri", function () {
const http = parseuri("http://google.com"),
https = parseuri("https://www.google.com:80"),
query = parseuri("google.com:8080/foo/bar?foo=bar"),
localhost = parseuri("localhost:8080"),
ipv6 = parseuri("2001:0db8:85a3:0042:1000:8a2e:0370:7334"),
ipv6short = parseuri("2001:db8:85a3:42:1000:8a2e:370:7334"),
ipv6port = parseuri("2001:db8:85a3:42:1000:8a2e:370:7334:80"),
ipv6abbrev = parseuri("2001::7334:a:80"),
ipv6http = parseuri("http://[2001::7334:a]:80"),
ipv6query = parseuri("http://[2001::7334:a]:80/foo/bar?foo=bar");

expect(http.protocol).to.be("http");
expect(http.port).to.be("");
expect(http.host).to.be("google.com");
expect(https.protocol).to.be("https");
expect(https.port).to.be("80");
expect(https.host).to.be("www.google.com");
expect(query.port).to.be("8080");
expect(query.query).to.be("foo=bar");
expect(query.path).to.be("/foo/bar");
expect(query.relative).to.be("/foo/bar?foo=bar");
expect(query.queryKey.foo).to.be("bar");
expect(query.pathNames[0]).to.be("foo");
expect(query.pathNames[1]).to.be("bar");
expect(localhost.protocol).to.be("");
expect(localhost.host).to.be("localhost");
expect(localhost.port).to.be("8080");
expect(ipv6.protocol).to.be("");
expect(ipv6.host).to.be("2001:0db8:85a3:0042:1000:8a2e:0370:7334");
expect(ipv6.port).to.be("");
expect(ipv6short.protocol).to.be("");
expect(ipv6short.host).to.be("2001:db8:85a3:42:1000:8a2e:370:7334");
expect(ipv6short.port).to.be("");
expect(ipv6port.protocol).to.be("");
expect(ipv6port.host).to.be("2001:db8:85a3:42:1000:8a2e:370:7334");
expect(ipv6port.port).to.be("80");
expect(ipv6abbrev.protocol).to.be("");
expect(ipv6abbrev.host).to.be("2001::7334:a:80");
expect(ipv6abbrev.port).to.be("");
expect(ipv6http.protocol).to.be("http");
expect(ipv6http.port).to.be("80");
expect(ipv6http.host).to.be("2001::7334:a");
expect(ipv6query.protocol).to.be("http");
expect(ipv6query.port).to.be("80");
expect(ipv6query.host).to.be("2001::7334:a");
expect(ipv6query.relative).to.be("/foo/bar?foo=bar");

const withUserInfo = parseuri("ws://foo:bar@google.com");

expect(withUserInfo.protocol).to.eql("ws");
expect(withUserInfo.userInfo).to.eql("foo:bar");
expect(withUserInfo.user).to.eql("foo");
expect(withUserInfo.password).to.eql("bar");
expect(withUserInfo.host).to.eql("google.com");

const relativeWithQuery = parseuri("/foo?bar=@example.com");

expect(relativeWithQuery.host).to.be("");
expect(relativeWithQuery.path).to.be("/foo");
expect(relativeWithQuery.query).to.be("bar=@example.com");
});
});

0 comments on commit 12b7d78

Please sign in to comment.