URL-Encoding 2026: Die Tücken von Prozent-Encoding, %20, + und doppelter Kodierung
URL-Encoding sieht harmlos aus: Ein Leerzeichen wird zu %20, ein Umlaut zu %C3%BC, fertig. In der Praxis ist es eine der häufigsten stillen Fehlerquellen in Web-Anwendungen. Mal steht das Leerzeichen als %20 da, mal als +. Umlaute zerfallen in zwei Prozent-Paare. Und wer einmal zu viel kodiert, produziert %2520 statt %20 – ein Bug, der bis zur Sicherheitslücke reicht. Dieser Artikel erklärt reserved und unreserved characters nach RFC 3986 und zeigt, wo die Fallen liegen.
Warum URL-Encoding überhaupt existiert
Eine URL ist im Kern eine Zeichenkette aus einem sehr kleinen, klar definierten Alphabet. Der Standard dafür ist RFC 3986 aus dem Jahr 2005, der bis heute (Stand Juli 2026) die maßgebliche Grundlage bildet. Er legt fest, welche Zeichen direkt in einer URL stehen dürfen und welche vorher umgeschrieben werden müssen. Alles, was nicht ins erlaubte Alphabet passt, wird per Prozent-Encoding in die Form %HH gebracht – ein Prozentzeichen gefolgt von zwei Hexadezimalziffern.
Der Grund ist praktisch: Eine URL wandert durch viele Systeme – Browser, Proxys, Webserver, Logdateien, E-Mail-Clients. Damit sie überall gleich interpretiert wird, darf sie nur aus einem robusten ASCII-Kern bestehen. Zeichen wie das Leerzeichen, das Fragezeichen oder ein Ampersand haben in einer URL strukturelle Bedeutung – sie trennen Bestandteile. Steht ein solches Zeichen in einem Wert, muss es kodiert werden, sonst zerbricht die Struktur.
Prozent-Encoding ist also kein Verschlüsselungs- oder Sicherheitsmechanismus, sondern reine Transportkodierung. Kodierte Daten sind trivial umkehrbar und bieten null Schutz; ihr einziger Zweck ist, beliebige Bytes sicher durch die enge Grammatik einer URL zu schleusen.
Reserved gegen unreserved: die Zeichenklassen aus RFC 3986
RFC 3986 teilt Zeichen in Gruppen ein, und dieser Unterschied ist der Schlüssel zum ganzen Thema. Die unreserved characters dürfen immer unkodiert stehen und dürfen niemals unnötig kodiert werden. Das sind genau: die Buchstaben A–Z und a–z, die Ziffern 0–9 sowie die vier Sonderzeichen -, ., _ und ~. Diese 66 Zeichen sind der sichere Kern jeder URL.
Die reserved characters dagegen haben eine strukturelle Rolle. Sie zerfallen in die gen-delims : / ? # [ ] @ und die sub-delims ! $ & ' ( ) * + , ; =. Ein / trennt Pfadsegmente, ein ? leitet die Query ein, ein # das Fragment, ein & trennt Query-Parameter. Als Trenner bleiben sie unkodiert; als Teil eines Wertes müssen sie prozent-kodiert werden – / wird zu %2F, ? zu %3F, & zu %26.
Genau hier entsteht die erste große Falle: Ob ein reserviertes Zeichen kodiert werden muss, hängt vom Kontext ab. Ein & im Nachnamen »Meyer & Sohn« muss im Query-Wert zu %26 werden, sonst hält der Server es für einen Parameter-Trenner; zwischen zwei Parametern bleibt es stehen. Dasselbe Zeichen, zwei Bedeutungen.
Das Leerzeichen-Problem: %20 gegen +
Kaum etwas verwirrt so zuverlässig wie die Frage, ob ein Leerzeichen zu %20 oder zu + wird. Beide sind korrekt – aber in unterschiedlichen Zusammenhängen. In einer echten URL nach RFC 3986 wird ein Leerzeichen zu %20. Das gilt im Pfad, im Fragment und grundsätzlich überall dort, wo die URL-Grammatik greift.
Das + als Leerzeichen stammt aus einem anderen, älteren Standard: dem Medientyp application/x-www-form-urlencoded, den HTML-Formulare beim Absenden verwenden. Dort – und nur dort – steht ein + für ein Leerzeichen, und ein echtes Pluszeichen muss deshalb zu %2B kodiert werden. Diese Regel ist Teil der HTML-Spezifikation und lebt bis heute im Query-String vieler Anwendungen fort.
Das Chaos entsteht, weil beide Konventionen im selben Query-String aufeinandertreffen. Ein Server, der den Query-Teil als x-www-form-urlencoded parst, macht aus a+b ein a b; ein anderer, der strikt nach RFC 3986 vorgeht, liefert wörtlich a+b zurück. Wer eine Suche mit echtem Pluszeichen baut – etwa »C++« – und es nicht als %2B kodiert, bekommt am anderen Ende womöglich »C « mit zwei Leerzeichen. Faustregel: Das Leerzeichen als %20 kodieren ist immer sicher, das Pluszeichen im Wert immer als %2B.
Umlaute und UTF-8: warum ü zu %C3%BC wird
Prozent-Encoding kennt nur Bytes, keine Buchstaben. Ein ü ist kein einzelnes Byte, sondern ein Unicode-Zeichen, das erst in Bytes übersetzt werden muss. Der Standard dafür ist UTF-8, den RFC 3986 für neue URI-Schemata ausdrücklich empfiehlt. In UTF-8 belegt ü zwei Bytes, 0xC3 und 0xBC; prozent-kodiert ergibt das %C3%BC – deshalb tauchen bei Umlauten immer zwei Prozent-Paare auf.
Das Muster zieht sich durch alle deutschen Sonderzeichen: ä wird zu %C3%A4, ö zu %C3%B6, ß zu %C3%9F, das große Ü zu %C3%9C. Zeichen außerhalb des lateinischen Bereichs brauchen mehr Bytes: Das Euro-Zeichen € wird zu drei Bytes %E2%82%AC, ein Emoji sogar zu vier. Je nach Zeichen wachsen die Prozent-Sequenzen also.
Die Falle heißt Zeichensatz-Mismatch. Kodiert die eine Seite in UTF-8 und dekodiert die andere in Latin-1 (ISO-8859-1), wird aus %C3%BC das berüchtigte Mojibake ü. Alte Latin-1-Systeme produzieren für ü ein einzelnes %FC, das ein UTF-8-Parser nicht versteht. Wer Umlaute zuverlässig transportieren will, muss auf beiden Seiten dasselbe Encoding sicherstellen – im Zweifel überall UTF-8.
encodeURIComponent gegen encodeURI in JavaScript
In JavaScript gibt es zwei eingebaute Funktionen zum Kodieren, und sie zu verwechseln ist einer der häufigsten Fehler überhaupt. encodeURI() ist dafür gedacht, eine komplette URL zu kodieren. Es lässt deshalb alle Zeichen unangetastet, die eine URL strukturell braucht: : / ? # [ ] @ ! $ & ' ( ) * + , ; = bleiben stehen. Das ist genau richtig, wenn man eine ganze Adresse hat, in der diese Zeichen als Trenner wirken sollen.
encodeURIComponent() dagegen ist für einen einzelnen Bestandteil gedacht – etwa den Wert eines Query-Parameters. Es kodiert deutlich mehr, unter anderem /, ?, &, = und #, und lässt nur die unreserved characters plus einige wenige Legacy-Zeichen (! ~ * ' ( )) stehen. Genau das braucht man, wenn ein Wert selbst Trennzeichen enthalten könnte.
Der klassische Bug: Man baut "...?q=" + encodeURI(begriff) und wundert sich, warum ein & oder = im Suchbegriff die Query zerreißt – encodeURI lässt diese Zeichen ja bewusst stehen. Richtig ist encodeURIComponent(begriff), weil der Begriff ein Wert ist. Umgekehrt darf man encodeURIComponent nicht auf eine ganze URL loslassen, sonst wird aus https:// ein unbrauchbares https%3A%2F%2F. Regel: encodeURI für ganze URLs, encodeURIComponent für einzelne Werte.
Wo im URL kodiert wird: Pfad, Query und Fragment
Eine URL besteht aus mehreren Zonen mit je eigenen Regeln. Im Pfad (nach dem Host bis zum ?) trennt der / die Segmente. Ein /, der zu einem Wert gehört und kein Trenner sein soll, muss zu %2F werden. Manche Webserver lehnen kodierte Slashes im Pfad aus Sicherheitsgründen sogar ab – ein Stolperstein bei der API-Gestaltung.
Die Query (nach dem ?) besteht aus Paaren schlüssel=wert, getrennt durch &. Hier ist die Kodierung am wichtigsten, weil Werte praktisch beliebigen Text enthalten können. Jeder =, jedes & und jedes # innerhalb eines Werts muss kodiert werden, sonst verschiebt sich die Parameter-Grenze. Genau in der Query lebt außerdem die +-gleich-Leerzeichen-Konvention, die den Pfad nicht betrifft.
Das Fragment (nach dem #) landet nie beim Server, sondern bleibt im Browser. Trotzdem gelten dieselben Zeichenregeln, und wer im Fragment etwa einen JSON-Zustand ablegt, muss dessen Sonderzeichen kodieren. Es für »egal« zu halten, weil es nicht übertragen wird, ist ein Irrtum – clientseitiger Code liest es sehr wohl aus.
Double Encoding: der Bug, der zur Sicherheitslücke wird
Der heimtückischste Fehler ist die doppelte Kodierung. Sie entsteht, wenn ein bereits kodierter Wert ein zweites Mal durch den Encoder läuft. Beim ersten Durchgang wird das Leerzeichen zu %20. Das Prozentzeichen darin ist selbst ein Sonderzeichen und wird beim zweiten Durchgang zu %25 – aus %20 wird also %2520. Dekodiert der Empfänger nur einmal, bekommt er wörtlich %20 statt eines Leerzeichens.
Praktisch passiert das ständig an Schnittstellen: Ein Frontend kodiert einen Wert, ein Framework kodiert die URL erneut, ein Proxy dekodiert einmal, der Server ein zweites Mal. Jede Schicht, die ungefragt kodiert oder dekodiert, kann die Balance kippen. Das Ergebnis sind kaputte Links, Dateinamen mit sichtbarem %20 im Text oder Suchtreffer, die nichts finden, weil der Suchbegriff %2520 lautet.
Sicherheitskritisch wird es, wenn ein Filter nur eine Dekodier-Runde erwartet. Ein Angreifer schreibt %252e%252e%252f statt ../. Der erste Dekodier-Schritt macht daraus %2e%2e%2f, was den Path-Traversal-Filter passiert; ein zweiter Schritt stellt dann ../ wieder her. Solche Double-Encoding-Bypässe stehen seit Jahren im OWASP-Testleitfaden und sind der Grund für konsequentes, einmaliges Kodieren an klaren Grenzen.
Häufige Fehler in der Praxis
Der erste Klassiker ist das Selbstbauen von Query-Strings per String-Verkettung ohne Kodierung der Werte. Sobald ein Wert ein &, = oder Leerzeichen enthält, bricht die Struktur. Sauber ist, jeden Wert einzeln mit encodeURIComponent (oder dem serverseitigen Äquivalent) zu behandeln – oder gleich eine URL-Builder-Bibliothek zu nutzen.
Der zweite ist das Kodieren zum falschen Zeitpunkt: Werte werden bereits kodiert in der Datenbank gespeichert und beim Ausliefern ein zweites Mal kodiert. Kodierung gehört an die Grenze zur URL, nicht in die Persistenz. In der Datenbank steht der reine Wert; kodiert wird erst, wenn er in eine konkrete URL wandert.
Der dritte betrifft Slugs. Wer einen Titel mit Umlauten direkt in eine URL steckt, bekommt %C3%BC-Ketten, die niemand lesen oder teilen mag. Besser ist ein sauberer Slug, der Umlaute zu ue, ae, oe auflöst und nur unreserved characters übrig lässt. Dann braucht die URL gar kein Encoding mehr.
Was Tools auf CalcSI helfen
Wenn du eine verdächtige oder kaputte URL vor dir hast, zerlegst du sie am schnellsten mit dem URL-Encoder/Decoder: Er zeigt dir, was hinter %C3%BC oder %2520 steckt, und deckt doppelte Kodierung auf, indem du zweimal dekodierst und schaust, ob sich noch etwas ändert. Für lesbare, encoding-freie Adressen erzeugst du aus Titeln mit Umlauten mit dem Slug-Generator saubere URL-Slugs, die ganz ohne Prozent-Sequenzen auskommen. Musst du Binärdaten robust in eine URL packen, ist oft Base64 die passendere Transportkodierung – achte auf die URL-sichere Variante, die + und / vermeidet. Und wenn du in Logs oder Eingaben gezielt nach Prozent-Mustern wie %25[0-9A-Fa-f]{2} suchen willst, baust und testest du den passenden Ausdruck im Regex-Tester. Alle vier laufen komplett im Browser, deine URLs verlassen den Rechner nicht.
Kommentare
Die Kommentare werden von Disqus bereitgestellt. Bevor sie geladen werden, brauchen wir deine Einwilligung — Disqus ist ein Drittanbieter und setzt eigene Cookies.