Fix URL encoding (#5557)

This commit is contained in:
Nayeem Rahman 2020-05-18 14:47:45 +01:00 committed by GitHub
parent c3ec16535f
commit 93c2164673
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 112 additions and 37 deletions

View file

@ -169,6 +169,42 @@ unitTest(function urlDriveLetter() {
assertEquals(new URL("http://example.com/C:").href, "http://example.com/C:");
});
unitTest(function urlHostnameUpperCase() {
assertEquals(new URL("https://EXAMPLE.COM").href, "https://example.com/");
});
unitTest(function urlTrim() {
assertEquals(new URL(" https://example.com ").href, "https://example.com/");
});
unitTest(function urlEncoding() {
assertEquals(
new URL("https://a !$&*()=,;+'\"@example.com").username,
"a%20!$&*()%3D,%3B+%27%22"
);
assertEquals(
new URL("https://:a !$&*()=,;+'\"@example.com").password,
"a%20!$&*()%3D,%3B+%27%22"
);
// FIXME: https://url.spec.whatwg.org/#idna
// assertEquals(
// new URL("https://a !$&*()=,+'\"").hostname,
// "a%20%21%24%26%2A%28%29%3D%2C+%27%22"
// );
assertEquals(
new URL("https://example.com/a ~!@$&*()=:/,;+'\"\\").pathname,
"/a%20~!@$&*()=:/,;+'%22/"
);
assertEquals(
new URL("https://example.com?a ~!@$&*()=:/,;?+'\"\\").search,
"?a%20~!@$&*()=:/,;?+%27%22\\"
);
assertEquals(
new URL("https://example.com#a ~!@#$&*()=:/,;?+'\"\\").hash,
"#a%20~!@#$&*()=:/,;?+'%22\\"
);
});
unitTest(function urlBaseURL(): void {
const base = new URL(
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"

View file

@ -11,7 +11,7 @@ interface URLParts {
hostname: string;
port: string;
path: string;
query: string | null;
query: string;
hash: string;
}
@ -53,7 +53,7 @@ function takePattern(string: string, pattern: RegExp): [string, string] {
function parse(url: string, isBase = true): URLParts | undefined {
const parts: Partial<URLParts> = {};
let restUrl;
[parts.protocol, restUrl] = takePattern(url, /^([a-z]+):/);
[parts.protocol, restUrl] = takePattern(url.trim(), /^([a-z]+):/);
if (isBase && parts.protocol == "") {
return undefined;
}
@ -61,9 +61,6 @@ function parse(url: string, isBase = true): URLParts | undefined {
parts.username = "";
parts.password = "";
[parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/);
if (parts.hostname.includes(":")) {
return undefined;
}
parts.port = "";
} else if (specialSchemes.includes(parts.protocol)) {
let restAuthority;
@ -80,7 +77,9 @@ function parse(url: string, isBase = true): URLParts | undefined {
restAuthentication,
/^([^:]*)/
);
parts.username = encodeUserinfo(parts.username);
[parts.password] = takePattern(restAuthentication, /^:(.*)/);
parts.password = encodeUserinfo(parts.password);
[parts.hostname, restAuthority] = takePattern(restAuthority, /^([^:]+)/);
[parts.port] = takePattern(restAuthority, /^:(.*)/);
if (!isValidPort(parts.port)) {
@ -92,10 +91,17 @@ function parse(url: string, isBase = true): URLParts | undefined {
parts.hostname = "";
parts.port = "";
}
try {
parts.hostname = encodeHostname(parts.hostname).toLowerCase();
} catch {
return undefined;
}
[parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/);
parts.path = parts.path.replace(/\\/g, "/");
parts.path = encodePathname(parts.path.replace(/\\/g, "/"));
[parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/);
parts.query = encodeSearch(parts.query);
[parts.hash] = takePattern(restUrl, /^(#.*)/);
parts.hash = encodeHash(parts.hash);
return parts as URLParts;
}
@ -259,9 +265,7 @@ export class URLImpl implements URL {
value = `#${value}`;
}
// hashes can contain % and # unescaped
parts.get(this)!.hash = escape(value)
.replace(/%25/g, "%")
.replace(/%23/g, "#");
parts.get(this)!.hash = encodeHash(value);
}
}
@ -282,7 +286,9 @@ export class URLImpl implements URL {
set hostname(value: string) {
value = String(value);
parts.get(this)!.hostname = encodeURIComponent(value);
try {
parts.get(this)!.hostname = encodeHostname(value);
} catch {}
}
get href(): string {
@ -319,7 +325,7 @@ export class URLImpl implements URL {
set password(value: string) {
value = String(value);
parts.get(this)!.password = encodeURIComponent(value);
parts.get(this)!.password = encodeUserinfo(value);
}
get pathname(): string {
@ -332,7 +338,7 @@ export class URLImpl implements URL {
value = `/${value}`;
}
// paths can contain % unescaped
parts.get(this)!.path = escape(value).replace(/%25/g, "%");
parts.get(this)!.path = encodePathname(value);
}
get port(): string {
@ -366,27 +372,13 @@ export class URLImpl implements URL {
}
get search(): string {
const query = parts.get(this)!.query;
if (query === null || query === "") {
return "";
}
return query;
return parts.get(this)!.query;
}
set search(value: string) {
value = String(value);
let query: string | null;
if (value === "") {
query = null;
} else if (value.charAt(0) !== "?") {
query = `?${value}`;
} else {
query = value;
}
parts.get(this)!.query = query;
const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`;
parts.get(this)!.query = encodeSearch(query);
this.#updateSearchParams();
}
@ -396,7 +388,7 @@ export class URLImpl implements URL {
set username(value: string) {
value = String(value);
parts.get(this)!.username = encodeURIComponent(value);
parts.get(this)!.username = encodeUserinfo(value);
}
get searchParams(): URLSearchParams {
@ -474,3 +466,56 @@ export class URLImpl implements URL {
blobURLMap.delete(url);
}
}
function charInC0ControlSet(c: string): boolean {
return c >= "\u0000" && c <= "\u001F";
}
function charInSearchSet(c: string): boolean {
// prettier-ignore
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u0027", "\u003C", "\u003E"].includes(c) || c > "\u007E";
}
function charInFragmentSet(c: string): boolean {
// prettier-ignore
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c);
}
function charInPathSet(c: string): boolean {
// prettier-ignore
return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c);
}
function charInUserinfoSet(c: string): boolean {
// "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox.
// prettier-ignore
return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c);
}
function encodeChar(c: string): string {
return `%${c.charCodeAt(0).toString(16)}`.toUpperCase();
}
function encodeUserinfo(s: string): string {
return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join("");
}
function encodeHostname(s: string): string {
// FIXME: https://url.spec.whatwg.org/#idna
if (s.includes(":")) {
throw new TypeError("Invalid hostname.");
}
return encodeURIComponent(s);
}
function encodePathname(s: string): string {
return [...s].map((c) => (charInPathSet(c) ? encodeChar(c) : c)).join("");
}
function encodeSearch(s: string): string {
return [...s].map((c) => (charInSearchSet(c) ? encodeChar(c) : c)).join("");
}
function encodeHash(s: string): string {
return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join("");
}

View file

@ -76,13 +76,7 @@ export class URLSearchParamsImpl implements URLSearchParams {
if (url == null) {
return;
}
let query: string | null = this.toString();
if (query === "") {
query = null;
}
parts.get(url)!.query = query;
parts.get(url)!.query = this.toString();
};
#append = (name: string, value: string): void => {