SuaveIO/suave

Equal signs in query parameter values are not parsed correctly

lydell opened this issue · 0 comments

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:

/// Parse the data in the string to a dictionary, assuming k/v pairs are separated
/// by the ampersand character.
let parseData (s : string) =
let parseArr (d : string array) =
if d.Length = 2 then (d.[0], Some <| System.Net.WebUtility.UrlDecode(d.[1]))
else d.[0],None
s.Split('&')
|> Array.toList
|> List.filter (not << String.IsNullOrWhiteSpace)
|> List.map (fun (k : string) -> k.Split('=') |> parseArr)