Live · CSR-certificeret
Tilbage til Hvad er en URL
Kapitel 01 · URL-anatomi

URL-encoding

En URL må kun bestå af omkring 80 forskellige tegn. Alt andet, inklusive æ, ø, å og mellemrum, bliver oversat til talkoder inden det forlader browseren.

Nøglefakta

Standard:RFC 3986
Format:%XX, to hexadecimale cifre per byte
Tegnsæt:UTF-8, så æ fylder to bytes
Domænenavnet:Punycode, ikke procent-kodning

Hvorfor findes procent-kodning?

En URL er defineret i RFC 3986 som en sekvens af tegn fra et begrænset US-ASCII-alfabet. Standarden blev skrevet i en tid hvor en byte var et tegn, og hvor systemerne der skulle sende URLer videre til hinanden ikke kunne regnes for at forstå andet end ASCII. Alt uden for det alfabet skal repræsenteres som et procenttegn efterfulgt af de to hexadecimale cifre der udgør bytens værdi.

Standarden deler tegnene i tre grupper. De unreserved tegn kan altid stå råt: bogstaverne A til Z i begge kasser, cifrene 0 til 9 samt bindestreg, punktum, underscore og tilde. De reserved tegn har en syntaktisk betydning i URLen: skråstreg adskiller stier, spørgsmålstegn starter query-strengen, hash starter fragmentet. Alt andet er uspecificeret og skal kodes.

unreserved   A-Z a-z 0-9 - . _ ~
gen-delims   : / ? # [ ] @
sub-delims   ! $ & ' ( ) * + , ; =

Pointen med opdelingen er at et reserved tegn betyder to forskellige ting afhængigt af om det er kodet. En rå skråstreg adskiller to stisegmenter. En kodet skråstreg, %2F, er en del af segmentets navn. Det er derfor du ikke bare kan kode hele URLen for en sikkerheds skyld: så ødelægger du dens struktur.

Danske bogstaver fylder to bytes

Moderne URLer kodes med UTF-8. I UTF-8 fylder ASCII-tegn én byte, mens æ, ø og å fylder to. Derfor bliver hvert af dem til to procent-sekvenser, ikke én. Det forklarer hvorfor en dansk URL ser voldsom ud når man ser den rå:

TegnUnicodeProcent-kodet
æU+00E6%C3%A6
øU+00F8%C3%B8
åU+00E5%C3%A5
ÆU+00C6%C3%86
ØU+00D8%C3%98
ÅU+00C5%C3%85
mellemrumU+0020%20
https://example.dk/produkter/rød-sofa
https://example.dk/produkter/r%C3%B8d-sofa

De to linjer er den samme URL. Browseren viser den øverste
og sender den nederste.

Browseren gør arbejdet automatisk. Skriver en bruger en dansk URL i adressefeltet, konverterer browseren den til den kodede form inden anmodningen sendes, og viser den afkodede form i adressefeltet bagefter. Google gør det samme i søgeresultaterne. Danske bogstaver i URLer er derfor ikke et SEO-problem i sig selv, men de er et driftsproblem, fordi de fordobler antallet af skrivemåder for den samme adresse.

%20 i stien, + i query-strengen

Her ligger den mest udbredte forvirring. Et mellemrum kodes som %20 efter RFC 3986. Men formulardata sendes efter en ældre og separat regel, application/x-www-form-urlencoded, hvor mellemrum kodes som plus. Begge regler er i brug i den samme URL på samme tid: den ene i stien, den anden i query-strengen.

Sti:            /soeg/r%C3%B8d%20sko
Query-streng:   /soeg?q=r%C3%B8d+sko

Begge betyder "rød sko". Kun i query-strengen betyder + et
mellemrum. I stien betyder + et plustegn.

Konsekvensen er praktisk. Skal du sende et rigtigt plustegn med som værdi i en query-streng, skal det kodes som %2B. Ellers læser modtageren det som et mellemrum. Det rammer typisk telefonnumre: ?tlf=+4532736149 bliver til tlf = 4532736149 med et mellemrum foran, og landekoden er væk.

?tlf=+4532736149     bliver læst som " 4532736149"
?tlf=%2B4532736149   bliver læst som "+4532736149"

encodeURI eller encodeURIComponent

JavaScript har to funktioner, og valget mellem dem afgør om din URL virker. encodeURI lader de reserved tegn stå, fordi den forventer at få en hel URL hvor de har syntaktisk betydning. encodeURIComponent koder dem, fordi den forventer at få én værdi der skal ind i URLen.

const query = 'rød & sort'

encodeURI(query)
// 'r%C3%B8d%20&%20sort'      <- & står stadig råt

encodeURIComponent(query)
// 'r%C3%B8d%20%26%20sort'    <- & er kodet til %26

// Kun den nederste er sikker som parameterværdi:
'/soeg?q=' + encodeURIComponent(query)

Reglen er enkel: koder du en del af en URL, brug encodeURIComponent. Bruger du encodeURI på en søgestreng der indeholder et og-tegn, splitter modtageren værdien i to parametre, og alt efter og-tegnet forsvinder ud af det felt det hørte til.

I praksis er den bedste løsning at lade et URL-objekt gøre arbejdet. Både browsere og Node har URLSearchParams, som koder korrekt uden at du skal huske hvilken funktion der hører til hvilken del:

const url = new URL('https://example.dk/soeg')
url.searchParams.set('q', 'rød & sort')
url.searchParams.set('kategori', 'møbler/sofaer')

url.toString()
// https://example.dk/soeg?q=r%C3%B8d+%26+sort&kategori=m%C3%B8bler%2Fsofaer

Punycode: domænenavnet følger andre regler

Procent-kodning virker i stien, query-strengen og fragmentet. Den virker ikke i værtsnavnet. DNS accepterer ikke procenttegn, så et domæne med danske bogstaver bliver i stedet oversat med punycode, defineret i RFC 3492 og indrammet af IDNA-standarden. Resultatet kendes på præfikset xn--.

hårplejelex.dk    ->  xn--hrplejelex-15a.dk
børn.dk           ->  xn--brn-0na.dk
æblegrød.dk       ->  xn--blegrd-oua3m.dk

Algoritmen fjerner de tegn der ikke er ASCII, og koder deres værdi og position i en hale efter to bindestreger. Derfor bliver navnet både uigenkendeligt og længere. Hvert label i domænet kodes for sig, så toplevel-delen .dk står uændret.

DK Hostmaster tillader æ, ø og å i .dk-domæner, og de fleste registratorer viser den læsbare form i deres brugerflade. Men certifikater, mailservere, logfiler og gamle systemer viser ofte punycode-formen. Registrerer du et domæne med danske bogstaver, så skriv xn---formen ned samme sted som resten af dine credentials. Den dukker op på det værste tidspunkt.

Punycode bruges også til phishing

Flere Unicode-tegn ligner latinske bogstaver til forveksling. Et kyrillisk а er ikke det samme tegn som et latinsk a, men de ser ens ud i en adresselinje. Browsere modvirker det ved at vise punycode-formen i stedet for den læsbare form når et domæne blander skriftsystemer. Ser du xn-- i adressefeltet på et domæne du troede var almindeligt, er det et advarselssignal, ikke en fejl i browseren.

Faldgruber i praksis

Dobbelt-encoding

Kodes en allerede kodet streng en gang til, bliver procenttegnet selv kodet. %C3%B8 bliver til %25C3%25B8, fordi %25 er koden for et procenttegn. Brugeren ser %C3%B8 stå som tekst i browservinduet i stedet for et ø.

Løsning: Kod ét sted i kodebasen. Sker det alligevel, er tegnet %25 i en URL næsten altid symptomet.

Uens skrivemåde giver to URLer

RFC 3986 siger at hexcifrene skal skrives med store bogstaver, og at afkodere skal acceptere begge. I praksis behandler nogle servere og CDN-cacher %C3%B8 og %c3%b8 som to forskellige stier. Så har du to cache-poster, to sæt logdata og potentielt to indekserede sider.

Løsning: Normaliser til store bogstaver på serveren, og sæt canonical til den ene form.

Kodet skråstreg i stien

%2F i stien afvises som standard af både Apache og nginx af sikkerhedshensyn, fordi den kan bruges til at snyde stivalidering. Bygger du en rute hvor en parameter indeholder en skråstreg, rammer du en 404 der ikke ser ud som en 404.

Løsning: Læg værdien i query-strengen i stedet, eller udskift skråstregen med et andet tegn inden den bliver til en sti.

Mellemrum i filnavne på upload

Et billede der uploades som Forside billede 2.jpg ender som Forside%20billede%202.jpg i hver eneste img-tag, i sitemappet og i alle delte links. Det virker, men det er svært at læse, det knækker i mails, og det bliver kopieret forkert.

Løsning: Normaliser filnavne til små bogstaver med bindestreger ved upload, inden filen når disken.

Best practices

Gør dette

  • • Brug URLSearchParams eller encodeURIComponent til værdier
  • • Skriv hexcifre med store bogstaver: %C3%B8, ikke %c3%b8
  • • Vælg én skrivemåde per URL og sæt canonical til den
  • • Translitterer slugs til ASCII: roed-sofa frem for rød-sofa
  • • Noter punycode-formen af IDN-domæner sammen med domænet
  • • Normaliser uploadede filnavne inden de bliver til URLer

Undgå dette

  • • encodeURI på en enkelt parameterværdi
  • • At kode en streng der allerede er kodet
  • • Plustegn som mellemrum i stien, det virker kun i query
  • • Rå plustegn i en parameterværdi uden %2B
  • • %2F i stien, servere afviser den som standard
  • • Interne links i både kodet og afkodet form på samme site

Kod og afkod fra kommandolinjen

Du behøver ikke et onlineværktøj. Både Python og Node kan gøre det i én linje, og det er hurtigere når du står i en fejlsøgning:

# Kod en værdi
python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "rød sofa"
# r%C3%B8d%20sofa

# Afkod igen
python3 -c "import urllib.parse,sys;print(urllib.parse.unquote(sys.argv[1]))" "r%C3%B8d%20sofa"
# rød sofa

# Punycode frem og tilbage
python3 -c "print('hårplejelex'.encode('idna').decode())"
# xn--hrplejelex-15a

Standarderne bag

  • RFC 3986 definerer URI-syntaksen: hvilke tegn der er unreserved, hvilke der er reserved, og hvordan procent-kodning skrives. Afsnit 2.1 er det der siger at hexcifre bør skrives med store bogstaver.
  • RFC 3492 beskriver punycode-algoritmen, altså hvordan et navn med Unicode-tegn oversættes til ren ASCII og tilbage igen.
  • RFC 5890 er rammen om IDNA2008 og fastlægger hvornår og hvordan punycode må bruges i domænenavne.
  • WHATWG URL Standard er den specifikation browsere faktisk implementerer. Den afviger enkelte steder fra RFC 3986, og det er den du skal slå op i når en browser gør noget uventet.
Læs videre

Relaterede opslag