A query string is the part of a URL after the ?. It carries extra information for the page or the server, such as a search term, a filter, or a page number, as key=value pairs joined by &. It runs to the end of the URL, or to a #, which starts the fragment, the part that points to a place within the page. Everything in it is text: page=2 carries the string "2". Here's a shop's search URL, https://shop.example/search?q=fish+%26+chips&tag=sale&tag=new&page=2, taken apart:
| Pair | Key | Value |
|---|---|---|
q=fish+%26+chips | q | fish & chips. Each + stands for a space, and %26 is an & that belongs to the value. |
tag=sale | tag | sale |
tag=new | tag | new. A key can appear more than once; that's a common way to send a list. |
page=2 | page | 2, as text |
The value fish & chips couldn't go in as it is: its & would be read as the start of a new pair, and a URL can't contain spaces. A # in a value would end the query too. Percent-encoding writes such a character as % followed by two hexadecimal digits, the value of its byte: a space is byte 32, or 20 in hex, so it becomes %20. A character outside ASCII (the basic English letters, digits, and symbols) is first turned into its bytes in UTF-8, the standard text encoding, so é, which takes two bytes, becomes %C3%A9. A % itself becomes %25. JavaScript's encodeURIComponent encodes one key or value this way, and decodeURIComponent reverses it:
encodeURIComponent("fish & chips"); // "fish%20%26%20chips"
encodeURIComponent("1+1=2"); // "1%2B1%3D2"
encodeURIComponent("café"); // "caf%C3%A9"
decodeURIComponent("caf%C3%A9"); // "café"
decodeURIComponent("fish+%26+chips"); // "fish+&+chips": it leaves + alone
Spaces have a second spelling. When an HTML form sends its fields in a query string, it writes each space as +, and most code that reads query strings, URLSearchParams included, reads every + as a space. So a real plus sign has to travel as %2B. encodeURIComponent writes spaces as %20, which every reader understands.
You meet query strings in search pages, filters, pagination, and tracking links, and you add them to fetch requests. Browsers and Node.js have URLSearchParams, also available as url.searchParams on a URL object, which reads and writes them for you.
Every plain object inherits from one shared object, Object.prototype: it's where methods like toString and hasOwnProperty come from. Prototype pollution is a security bug in which input from outside the program, such as a query string, a form field's name, or a JSON request body, gets code to write a property onto Object.prototype. From then on, every object in the program seems to have that property.
The way in is a few special key names. On a plain object, obj.__proto__ isn't an ordinary property: it's a getter and setter, inherited from Object.prototype, that reads or replaces the object's prototype. So obj["__proto__"] is Object.prototype itself, and a property written through it is written for everyone:
const settings = {};
const key = "__proto__"; // imagine it came from a query string
settings[key].isAdmin = true; // settings.__proto__ is Object.prototype
({}).isAdmin; // true: every object has it now
const user = { name: "Ada" };
user.isAdmin; // true, though user never had the property
delete Object.prototype.isAdmin; // clean up
constructor and prototype lead to the same place: obj.constructor is Object, and Object.prototype is the shared prototype. Here's where a function that sets a value at a dotted path would write, if it followed each key as given:
| Path | Following it reaches | The value lands on |
|---|---|---|
user.isAdmin | obj.user, an ordinary nested object | obj.user, as intended |
__proto__.isAdmin | obj.__proto__, which is Object.prototype | Object.prototype, so every object |
constructor.prototype.isAdmin | obj.constructor, which is Object, then Object.prototype | Object.prototype, so every object |
Why it's a security bug: code often checks a property that's normally missing, like if (user.isAdmin), and after pollution every object passes. Replacing an inherited method, such as toString, can also make code all over the app throw. Bugs like this have been found and fixed in popular libraries, Lodash and jQuery among them.
One more detail. Assigning obj["__proto__"] = value on a plain object doesn't create a key named __proto__: it replaces obj's prototype with value, or does nothing if value isn't an object or null. JSON.parse, though, does create an ordinary key named __proto__, so parsed JSON can carry one into code that copies or merges keys:
const parsed = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.keys(parsed); // ["__proto__"]: a real own key, harmless so far
const target = {};
target["__proto__"] = parsed["__proto__"]; // replaces target's prototype
Object.keys(target); // []: no key was created
target.isAdmin; // true, inherited from its new prototype
Write parseQueryString(query). It turns a URL query string into an object that maps each key to its value. Write the parsing yourself, without URLSearchParams.
Ignore one leading ?. Split the rest into pairs at &, skipping empty pairs such as the ones && or a trailing & leave. Split each pair at its first =: what comes before is the key, and everything after is the value, even if it contains more =. A pair with no = is a key whose value is "".
Decode the key and the value after splitting: first turn every + into a space, then decode percent-escapes with decodeURIComponent, so %26 is a literal & and %2B a literal +. If a key or value has malformed percent-encoding, keep it as it is (with its + turned into spaces) instead of throwing.
A key that appears once maps to its string value; a key that appears more than once maps to an array of its values, in order. Every key, __proto__ included, must become an ordinary own property of the result, so a query can never change the object's prototype.
parseQueryString("?q=red+shoes&tag=a&tag=b&debug");
// { q: "red shoes", tag: ["a", "b"], debug: "" }
expect(parseQueryString("?a=1&b=two")).toEqual({ a: "1", b: "two" });
expect(parseQueryString("a=1")).toEqual({ a: "1" });
expect(parseQueryString("q=red+shoes&city=S%C3%A3o%20Paulo&sum=1%2B1")).toEqual({ q: "red shoes", city: "São Paulo", sum: "1+1" });
expect(parseQueryString("a=1%262&b%3Dc=x&first+name=Ada")).toEqual({ a: "1&2", "b=c": "x", "first name": "Ada" });
expect(parseQueryString("token=abc==&eq=a=b")).toEqual({ token: "abc==", eq: "a=b" });
expect(parseQueryString("debug&&a=&")).toEqual({ debug: "", a: "" });
expect(parseQueryString("")).toEqual({});
expect(parseQueryString("?")).toEqual({});
expect(parseQueryString("tag=a&x=1&tag=b&tag=c")).toEqual({ tag: ["a", "b", "c"], x: "1" });
expect(parseQueryString("k=1&k=1")).toEqual({ k: ["1", "1"] });
expect(parseQueryString("a=100%&b=%E0%A4%A&c=%41&d=50%+off")).toEqual({ a: "100%", b: "%E0%A4%A", c: "A", d: "50% off" });
const result = parseQueryString("__proto__=x&__proto__=y&a=1");
expect(Object.keys(result)).toEqual(["__proto__", "a"]);
expect(Object.getOwnPropertyDescriptor(result, "__proto__").value).toEqual(["x", "y"]);
expect(Array.isArray(Object.getPrototypeOf(result))).toBe(false);
expect(result.a).toBe("1");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Parse a query string. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.