SuaveIO / suave

Suave is a simple web development F# library providing a lightweight web server and a set of combinators to manipulate route flow and task composition.
https://suave.io
Other
1.32k stars 198 forks source link

Equal signs in query parameter values are not parsed correctly #774

Open lydell opened 1 year ago

lydell commented 1 year ago

This is a valid URL:

http://example.com/?q=a=b

It has a query parameter called q with the value a=b.

ctx.request.query does not contain the q parameter at all though – it’s dropped. Because of the equals sign in the value.

A workaround is to escape that equals sign:

http://example.com/?q=a%3Db

Here’s how System.Web.HttpUtility.ParseQueryString, Python and Node.js/browsers parse it (they all handle unescaped equal signs in query paramter values):

❯ dotnet fsi

Microsoft (R) F# Interactive version 12.4.0.0 for F# 7.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> System.Web.HttpUtility.ParseQueryString("?q=a=b").Get("q");;
val it: string = "a=b"

❯ python3
Python 3.11.2 (main, Feb 16 2023, 02:55:59) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import urllib.parse
>>> urllib.parse.parse_qs("q=a=b")["q"]
['a=b']

❯ node
Welcome to Node.js v16.17.0.
Type ".help" for more information.
> new URLSearchParams("?q=a=b").get("q")
'a=b'

I believe this is the relevant section in the WHATWG URL spec:

https://url.spec.whatwg.org/#urlencoded-parsing

If bytes contains a 0x3D (=), then let name be the bytes from the start of bytes up to but excluding its first 0x3D (=), and let value be the bytes, if any, after the first 0x3D (=) up to the end of bytes. If 0x3D (=) is the first byte, then name will be the empty byte sequence. If it is the last, then value will be the empty byte sequence.

This code splits on = but only uses the value if the splitted list has length 2. It needs to either use the head as the key, and join the tail back up with =, or split just on the first equals sign: https://github.com/SuaveIO/suave/blob/8efe4b32ea0dc52f36c10c8d8fec8191c6ae901c/src/Suave/Utils/Parsing.fs#L18-L27