{"id":21809,"date":"2025-01-02T16:07:31","date_gmt":"2025-01-02T16:07:31","guid":{"rendered":"https:\/\/lemire.me\/blog\/?p=21809"},"modified":"2025-02-08T01:38:26","modified_gmt":"2025-02-08T01:38:26","slug":"how-does-your-url-parser-handle-unicode","status":"publish","type":"post","link":"https:\/\/lemire.me\/blog\/2025\/01\/02\/how-does-your-url-parser-handle-unicode\/","title":{"rendered":"How does your URL parser handle Unicode?"},"content":{"rendered":"<p>Most strings today in software are Unicode strings. It means that you can include mathematical symbols, emojis and so forth. There are many different versions of the letter &#8216;M&#8217;, for example: the Roman letter M (U+004D) is semantically different from the Roman numeral \u216f (U+216F) while they both often have the same visual representation. John Cook has an interesting post on <a href=\"https:\/\/www.johndcook.com\/blog\/2025\/01\/02\/unicode-stegonography\/\"><em>Unicode Stegonography<\/em><\/a>: you can possibly use this ambiguity to hide messages in plain view. E.g., if you need to warn someone that you are in danger, you could send a text with the Roman numeral M. Normal people reading the text would not notice the difference.<\/p>\n<p>What about URLs like Microsoft.com? What if you replace the Roman letter by a Roman numeral, is it still the same domain?<\/p>\n<p>It is. URL parsers are required to normalize the URLs which involves, among other things, replacing look-alike letters with Roman letters if they are to be compliant with the WHATWG URL specification.<\/p>\n<p>But do they? Do the URL parsers actually do this hard work? Let us check.<\/p>\n<p><strong>Java<\/strong>. I could not get the standard Java library to return to me the host. It simply returns a null String.<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\"> <span style=\"color: #603000;\">String<\/span> url <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\"> URI uri <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000; font-weight: bold;\">new<\/span> URI<span style=\"color: #808030;\">(<\/span>url<span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\"> <span style=\"color: #603000;\">String<\/span> host <span style=\"color: #808030;\">=<\/span> uri<span style=\"color: #808030;\">.<\/span>getHost<span style=\"color: #808030;\">(<\/span><span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;<\/span><\/span><\/pre>\n<p><strong>C#<\/strong>. The .NET library seems to just returns the domain as-is with the Roman numeral when using the Host attribute, but it works correct with the IdnHost property.<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\"><span style=\"color: #603000;\">string<\/span> url <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\">Uri uri <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000; font-weight: bold;\">new<\/span> Uri<span style=\"color: #808030;\">(<\/span>url<span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #603000;\">string<\/span> host <span style=\"color: #808030;\">=<\/span> uri<span style=\"color: #808030;\">.<\/span>IdnHost<span style=\"color: #800080;\">;<\/span><\/span><\/pre>\n<p><strong>PHP<\/strong>. The standard PHP interpreter just returns the domain as-is, with the Roman numeral<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\">$url <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\">$parsed_url <span style=\"color: #808030;\">=<\/span> parse_url<span style=\"color: #808030;\">(<\/span>$url<span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;\r\n<span style=\"color: #800000; font-weight: bold;\">if<\/span> <span style=\"color: #808030;\">(<\/span>$parsed_url <span style=\"color: #808030;\">=<\/span><span style=\"color: #808030;\">=<\/span><span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000; font-weight: bold;\">false<\/span><span style=\"color: #808030;\">)<\/span> {\r\n echo <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">URL could not be parsed.<\/span><span style=\"color: #800000;\">\"<\/span>;\r\n} <span style=\"color: #800000; font-weight: bold;\">else<\/span> {\r\n $host <span style=\"color: #808030;\">=<\/span> $parsed_url<span style=\"color: #808030;\">[<\/span><span style=\"color: #0000e6;\">'host'<\/span><span style=\"color: #808030;\">]<\/span>;\r\n}\r\n<\/span><\/span>\r\n\r\n<\/pre>\n<p><strong>Go<\/strong>. Go also does not do normalization.<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\">urlString <span style=\"color: #800080;\">:<\/span><span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><\/span>\r\n<span class=\"line_wrapper\">parsedURL<span style=\"color: #808030;\">,<\/span> err <span style=\"color: #800080;\">:<\/span><span style=\"color: #808030;\">=<\/span> url<span style=\"color: #808030;\">.<\/span>Parse<span style=\"color: #808030;\">(<\/span>urlString<span style=\"color: #808030;\">)<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #800000; font-weight: bold;\">if<\/span> err <span style=\"color: #808030;\">!<\/span><span style=\"color: #808030;\">=<\/span> nil <span style=\"color: #800080;\">{<\/span><\/span>\r\n<span class=\"line_wrapper\">        fmt<span style=\"color: #808030;\">.<\/span>Println<span style=\"color: #808030;\">(<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">URL could not be parsed:<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #808030;\">,<\/span> err<span style=\"color: #808030;\">)<\/span><\/span>\r\n<span class=\"line_wrapper\">        <span style=\"color: #800000; font-weight: bold;\">return<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #800080;\">}<\/span><\/span>\r\n<span class=\"line_wrapper\">host <span style=\"color: #800080;\">:<\/span><span style=\"color: #808030;\">=<\/span> parsedURL<span style=\"color: #808030;\">.<\/span>Host<\/span><\/pre>\n<p><strong>Python<\/strong>. You guessed it: no normalization. It happily returns the Roman numeral.<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\">url <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><\/span>\r\n<span class=\"line_wrapper\">parsed_url <span style=\"color: #808030;\">=<\/span> urllib<span style=\"color: #808030;\">.<\/span>parse<span style=\"color: #808030;\">.<\/span>urlparse<span style=\"color: #808030;\">(<\/span>url<span style=\"color: #808030;\">)<\/span><\/span>\r\n<span class=\"line_wrapper\">host <span style=\"color: #808030;\">=<\/span> parsed_url<span style=\"color: #808030;\">.<\/span>netloc<\/span><\/pre>\n<p><strong>JavaScript<\/strong>. JavaScript does it correctly. It will convert https:\/\/microsoft.co\u216f to https:\/\/microsoft.com.<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\"><span style=\"color: #800000; font-weight: bold;\">const<\/span> url <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #800000; font-weight: bold;\">const<\/span> urlObj <span style=\"color: #808030;\">=<\/span> <span style=\"color: #800000; font-weight: bold;\">new<\/span> URL<span style=\"color: #808030;\">(<\/span>url<span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #800000; font-weight: bold;\">const<\/span> host <span style=\"color: #808030;\">=<\/span> urlObj<span style=\"color: #808030;\">.<\/span>hostname<span style=\"color: #800080;\">;<\/span><\/span><\/pre>\n<p><strong>C++<\/strong>. C++ does not have a standard URL parser, but if you use the <a href=\"https:\/\/ada-url.com\">ada URL parser,<\/a> you will get correct results. If you are using the Node.js runtime environment, the underlying parser is the C++ ada URL parsing library.<\/p>\n<pre class=\"code_syntax\" style=\"color: #000000; background: #ffffff;\"><span class=\"line_wrapper\"><span style=\"color: #800000; font-weight: bold;\">auto<\/span> url <span style=\"color: #808030;\">=<\/span> ada<span style=\"color: #800080;\">::<\/span>parse<span style=\"color: #808030;\">(<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #0000e6;\">https:\/\/microsoft.co\u216f<\/span><span style=\"color: #800000;\">\"<\/span><span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #800000; font-weight: bold;\">if<\/span> <span style=\"color: #808030;\">(<\/span><span style=\"color: #808030;\">!<\/span>url<span style=\"color: #808030;\">)<\/span> <span style=\"color: #800080;\">{<\/span> <span style=\"color: #696969;\">\/* failure *\/<\/span> <span style=\"color: #800080;\">}<\/span><\/span>\r\n<span class=\"line_wrapper\"><span style=\"color: #666616;\">std<\/span><span style=\"color: #800080;\">::<\/span>string_view host <span style=\"color: #808030;\">=<\/span> url<span style=\"color: #808030;\">-<\/span><span style=\"color: #808030;\">&gt;<\/span>get_host<span style=\"color: #808030;\">(<\/span><span style=\"color: #808030;\">)<\/span><span style=\"color: #800080;\">;<\/span><\/span><\/pre>\n<p><strong>Further reading<\/strong>:<a href=\"https:\/\/i.blackhat.com\/USA-19\/Thursday\/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization-wp.pdf\"> Host\/Split: Exploitable Antipatterns in Unicode Normalization<\/a> by Jonathan Birch (Microsoft), <a href=\"https:\/\/www.schneier.com\/blog\/archives\/2005\/02\/unicode_url_hac_1.html\">Unicode URL Hack<\/a> by Bruce Schneier, <a href=\"https:\/\/adambates.org\/documents\/Reynolds_Esorics22.pdf\">Equivocal URLs: Understanding the Fragmented Space of URL Parser Implementations<\/a>. (European Symposium on Research in Computer Security 2022), NorthSec 2020 \u2013 Philippe Arteau \u2013 <a href=\"https:\/\/www.youtube.com\/watch?v=Mqg8e6lJ-E8\">Unicode vulnerabilities that could by\u0365te you.<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Most strings today in software are Unicode strings. It means that you can include mathematical symbols, emojis and so forth. There are many different versions of the letter &#8216;M&#8217;, for example: the Roman letter M (U+004D) is semantically different from the Roman numeral \u216f (U+216F) while they both often have the same visual representation. John &hellip; <a href=\"https:\/\/lemire.me\/blog\/2025\/01\/02\/how-does-your-url-parser-handle-unicode\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">How does your URL parser handle Unicode?<\/span><\/a><\/p>\n","protected":false},"author":56,"featured_media":21879,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[84],"tags":[],"class_list":["post-21809","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-84"],"_links":{"self":[{"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/posts\/21809","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/users\/56"}],"replies":[{"embeddable":true,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/comments?post=21809"}],"version-history":[{"count":5,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/posts\/21809\/revisions"}],"predecessor-version":[{"id":21823,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/posts\/21809\/revisions\/21823"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/media\/21879"}],"wp:attachment":[{"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/media?parent=21809"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/categories?post=21809"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lemire.me\/blog\/wp-json\/wp\/v2\/tags?post=21809"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}