Jeder Teil einer URL und was darin erlaubt ist
Eine URL ist eine einzige Zeichenkette, die angibt, welches Protokoll gesprochen wird, mit welcher Maschine es gesprochen wird und wonach gefragt wird. Sie richtig zu lesen ist vor allem eine Frage davon, zu wissen, wo jeder Teil endet, denn die Trennzeichen sind einzelne Zeichen und das erste gewinnt.
https://alex:s3cret@www.example.co.uk:8443/docs/intro?lang=en&page=2#notes
│ │ │ │ │ │ └─ fragment
│ │ │ │ │ └──────────────── query
│ │ │ │ └──────────────────────────── path
│ │ │ └──────────────────────────────── port
│ │ └────────────────────────────────────────────────── host
│ └────────────────────────────────────────────────────────────── userinfo
└────────────────────────────────────────────────────────────────────── scheme
Die Teile
| Teil | Begrenzt durch | Wofür er da ist | Erreicht den Server |
|---|---|---|---|
| Schema | endet beim ersten : | Welches Protokoll gilt und welche Regeln für den Rest gelten | Bestimmt die Verbindung |
| Userinfo | nach //, endet bei @ | Zugangsdaten, für http und https seit Langem abgelehnt | Nur wenn der Client sie senden will |
| Host | endet bei :, /, ? oder # | Ein Domainname oder eine IP-Adresse, die aufgelöst und verbunden wird | Ja, im Host-Header |
| Port | nach :, endet bei /, ? oder # | Der TCP-Port, standardmäßig 80 für http und 443 für https | Wird für die Verbindung genutzt |
| Pfad | beginnt bei /, endet bei ? oder # | Welche Ressource auf diesem Host | Ja, in der Request-Zeile |
| Query | beginnt bei ?, endet bei # | Parameter für diese Ressource | Ja, in der Request-Zeile |
| Fragment | beginnt bei #, läuft bis zum Ende | Eine Stelle innerhalb der Ressource | Nein |
Host und Port zusammen bilden die Authority. Das Fragment ist der Sonderfall: Es wird entfernt, bevor der Request gebaut wird, ein Server sieht es also nie. Browser nutzen es für Anker und clientseitiges Routing, und manche Authentifizierungsabläufe geben Tokens absichtlich im Fragment zurück, damit diese Tokens nie in einem Server-Log auftauchen. Es landet trotzdem im Browserverlauf, ist also nicht privat, sondern nur ungesendet.
Reservierte und nicht reservierte Zeichen
Prozentcodierung schreibt ein Byte als % plus zwei Hexziffern und verwendet dafür die UTF-8-Bytes des Zeichens. é wird zu %C3%A9, zwei Bytes und zwei Escapes.
Neben Buchstaben und Ziffern sind vier Zeichen nicht reserviert und müssen nirgendwo codiert werden: - . _ ~. Alles andere ist entweder reserviert, hat also irgendwo in einer URL eine strukturelle Aufgabe, oder muss codiert werden. Die reservierte Menge ist : / ? # [ ] @ sowie ! $ & ' ( ) * + , ; =.
Das Wort „reserviert“ bedeutet nicht „immer verboten“. Es bedeutet, dass das Zeichen in irgendeinem Teil ein Trennzeichen ist und nur in den Teilen codiert werden muss, in denen es als solches gelesen würde.
| Zeichen | In einem Pfadsegment | In einem Query-Wert |
|---|---|---|
| Leerzeichen | %20, immer | %20 oder + |
/ | %2F, sonst teilt es das Segment | So zulässig |
? | %3F, sonst endet dort der Pfad | So zulässig |
# | %23, immer | %23, immer |
& und = | So zulässig | %26 und %3D, sonst zerfällt das Paar |
+ | Zulässig und bedeutet ein Plus | %2B, sonst wird es womöglich als Leerzeichen decodiert |
Das ist die praktische Regel: Codiere das Zeichen, das den Teil beenden würde, in dem du gerade bist. Ein Leerzeichen in einem Pfad muss %20 sein, weil ein rohes Leerzeichen in den meisten Parsern die URL beendet, während ein Leerzeichen in einem Query-Wert %20 oder das ältere + sein kann. Ein URL-Encoder ist der zuverlässige Weg dorthin, denn eine ganze URL zu codieren und eine einzelne Komponente zu codieren sind verschiedene Operationen, und fast immer ist die zweite gemeint.
Query-Strings sind eine Konvention
Die Spezifikation sagt nur, dass die Query alles zwischen ? und # ist, gebildet aus den erlaubten Zeichen. Die Form key=value&key=value stammt aus der HTML-Formularcodierung, nicht aus dem URI-Standard. Nichts hindert einen Server daran, ?a:1;b:2 so zu parsen, wie es ihm gefällt.
Weil die Form nur Konvention ist, haben wiederholte Schlüssel keine definierte Bedeutung, und jeder Stack hat sich seine eigene Antwort ausgesucht:
Verhalten bei ?id=1&id=2 | Wo |
|---|---|
| Beide Werte, als Liste | Python parse_qs, Node querystring, Express |
| Nur der erste Wert | Go Query().Get, Java getParameter |
| Nur der letzte Wert | PHP, Rails |
Zu 1,2 zusammengefügt | ASP.NET-Request-Collections |
Arrays erben dasselbe Problem. ids=1&ids=2, ids[]=1&ids[]=2, ids[0]=1&ids[1]=2 und ids=1,2 sind alle weit verbreitet, und nur der Server entscheidet, welche Form er versteht. Klammernotation muss codiert werden, um streng genommen zulässig zu sein (ids%5B%5D=1), auch wenn die meisten Server die rohen Klammern akzeptieren. Wähle die Form, die die empfangende Seite dokumentiert, und bleib dann dabei.
Das Pluszeichen verdient eine eigene Warnung. In application/x-www-form-urlencoded-Daten, also einem formularcodierten Request-Body, bedeutet + ein Leerzeichen. In einem URL-Pfad bedeutet es ein wörtliches Plus. In einem Query-String hängt es vom Parser ab, und die meisten Web-Frameworks wenden dort die Formulardecodierung an, sodass + stillschweigend zu einem Leerzeichen wird. Jeder Wert, der berechtigterweise ein Plus enthalten kann, etwa eine Standard-Base64-Ausgabe oder eine Telefonnummer, muss als %2B gesendet werden.
Relative Referenzen
Eine relative Referenz wird gegen eine Basis-URL aufgelöst, indem alles nach dem letzten / der Basis ersetzt wird. Dieser letzte Schrägstrich ist die ganze Regel, und deshalb macht ein abschließender Schrägstrich einen Unterschied.
| Basis | Referenz | Ergebnis |
|---|---|---|
https://ex.com/docs/intro | guide | https://ex.com/docs/guide |
https://ex.com/docs/intro/ | guide | https://ex.com/docs/intro/guide |
https://ex.com/docs/intro | /guide | https://ex.com/guide |
https://ex.com/docs/intro/ | ../guide | https://ex.com/docs/guide |
https://ex.com/docs/intro?a=1 | ?b=2 | https://ex.com/docs/intro?b=2 |
https://ex.com/docs/intro?a=1 | #top | https://ex.com/docs/intro?a=1#top |
https://ex.com/docs/intro | //cdn.ex.com/x.js | https://cdn.ex.com/x.js |
Die letzte Zeile ist eine schemarelative Referenz: Zwei führende Schrägstriche behalten das Schema der Basis und ersetzen die Authority. Eine Referenz, die mit ? beginnt, behält den Pfad und verwirft die alte Query, und eine, die mit # beginnt, behält beides.
Wann zwei URLs dieselbe Ressource sind
Schema und Host unterscheiden nicht zwischen Groß- und Kleinschreibung, HTTPS://Example.COM und https://example.com sind also eine Adresse. Alles nach dem Host ist aus Sicht des Standards case-sensitiv, auch wenn ein bestimmter Server die Schreibweise ignoriert.
Diese Paare sind gleichwertig:
https://example.com:443/aundhttps://example.com/a, weil der Port der Standardport isthttps://example.comundhttps://example.com/, weil ein leerer Pfad die Wurzel bedeutet/a/%7Euserund/a/~user, weil~nicht reserviert ist und die Prozentcodierung daran nichts ändert
Diese Paare nicht:
/docsund/docs/, das sind verschiedene Ressourcen, auch wenn die meisten Server die eine auf die andere umleiten/p?a=1&b=2und/p?b=2&a=1, denn die Reihenfolge der Parameter gehört zur Zeichenkette/index.htmlund/, sofern der Server nichts anderes sagt
Caches und CDNs schlüsseln auf die exakten Bytes, ein zusätzlicher Tracking-Parameter oder eine umsortierte Query teilt ein Cache-Objekt also in zwei und halbiert die Trefferquote. Suchmaschinen behandeln die Varianten als doppelte Seiten, sofern kein Canonical-Link auf eine gewählte Form zeigt. Die Lösung besteht darin, eine einzige Form festzulegen, den Rest per 301 dorthin umzuleiten und Parameter zu entfernen, die die Antwort nicht verändern.
Länge, Logs und Privatsphäre
In der Spezifikation gibt es keine Längenbegrenzung, überall sonst aber schon. Übliche Server-Standardwerte begrenzen die gesamte Request-Zeile auf etwa 8 KB, manche Proxys und Appliances auf 4 KB und ältere Clients auf rund 2 KB. Was E-Mail-Clients, QR-Codes und Linkverkürzer überstehen muss, ist unter etwa 2000 Zeichen am sichersten.
Das stärkere Argument für kurze URLs ist, dass ein Query-String nicht privat ist. Er landet in Access-Logs, wird im Referer-Header an Dritte weitergegeben, bleibt im Browserverlauf, wird mit Lesezeichen gespeichert und mitkopiert, sobald jemand den Link teilt. Session-Tokens, Codes zum Zurücksetzen von Passwörtern, API-Schlüssel und personenbezogene Daten gehören deshalb nicht dorthin. Große oder sensible Daten gehören in einen Request-Body, der keine praktische Größenobergrenze hat und standardmäßig nicht geloggt wird.
Einen Link lesen, bevor du ihn anklickst
Die Authority beginnt nach :// und endet beim allerersten /, ? oder #. Lies diesen Abschnitt und darin die letzten zwei oder drei Labels. Das ist der echte Host, und nichts links davon ändert daran etwas.
https://www.paypal.com@198.51.100.7/secure/login
^^^^^^^^^^^^^^ ^^^^^^^^^^^^
userinfo real host
Übliche Verkleidungen, die du erkennen solltest:
- Text vor einem
@ist Userinfo, niemals der Host, und kann ein beliebiger Markenname sein. paypal.com.secure-login.exampleist eine Subdomain vonsecure-login.example. Labels werden von rechts nach links gelesen.https://short.example/https://www.bank.com/loginsteckt eine ganze überzeugend wirkende URL in den Pfad.- Ein Host, der mit
xn--beginnt, ist Punycode, die ASCII-Form eines internationalisierten Domainnamens, erzeugt durch IDNA.münchen.deist auf der Leitungxn--mnchen-3ya.de, was legitim ist, aber derselbe Mechanismus ermöglicht Homographen: Das kyrillischeа(U+0430) ist optisch nicht vom lateinischenazu unterscheiden, und eine Domain, die es verwendet, codiert zu so etwas wiexn--pple-43d.com. Browser zeigen die Punycode-Form an, wenn ein Label Schriftsysteme mischt, doch die Regeln unterscheiden sich und sind keine Garantie. - Codierte Trennzeichen wie
%2Foder%40innerhalb eines Hosts sind ein bewusster Versuch, einen Parser zu verwirren.
Den Link in einen URL-Parser einzufügen klärt die Sache in einem Schritt, denn ein echter Parser wendet dieselbe Regel an wie der Browser, nach der das erste Trennzeichen gewinnt, und zeigt den Host für sich allein. Um viele Links auf einmal zu prüfen, ist ein Regex-Tester ein schneller Weg, um festzustellen, welche davon tatsächlich die Authority haben, die du erwartest.