How to Decode a URL Parameter That Contains Another URL
Tracking links and login redirects nest one URL inside another, encoded once per layer. Here is how to unwrap them correctly in JavaScript and Python, and the redirect check that keeps it safe.
To decode a URL parameter that contains another URL, you need to unwrap one layer at a time. The URL Decoder decodes text in the browser, and its page explains how to tell how many times something was encoded. This guide works through a real-world shape: a tracking link that wraps a login link that wraps the page you actually want.
A URL inside a URL inside a URL
An email links to a click tracker. The tracker forwards to a login page, and the login page redirects to the cart once you sign in. Each hop carries the next one as an encoded parameter:
https://t.example.com/c?u=https%3A%2F%2Fauth.example.com%2Flogin%3Fredirect_uri%3Dhttps%253A%252F%252Fshop.example.com%252Fcart%253Fcoupon%253DSAVE%252010%2526ref%253DemailThe inner URL was encoded twice, once for each level it is nested in, which is why it shows %253A instead of %3A: %25 is an encoded %.
Decode URL parameters in JavaScript
Let the URL object do the decoding. searchParams.get decodes exactly one layer, which is exactly what each step needs:
const login = new URL(tracking).searchParams.get("u");
// "https://auth.example.com/login?redirect_uri=https%3A%2F%2Fshop.example.com%2Fcart%3Fcoupon%3DSAVE%2010%26ref%3Demail"
const cart = new URL(login).searchParams.get("redirect_uri");
// "https://shop.example.com/cart?coupon=SAVE 10&ref=email"Do not call decodeURIComponent on top of searchParams.get. The value is already decoded once, and a second decode turns any real % in the data, such as a coupon named 50%OFF, into a broken escape or an error.
Decode URL parameters in Python
from urllib.parse import parse_qs, urlsplit
login = parse_qs(urlsplit(tracking).query)["u"][0]
cart = parse_qs(urlsplit(login).query)["redirect_uri"][0]
# 'https://shop.example.com/cart?coupon=SAVE 10&ref=email'parse_qs decodes one layer and returns a list per key, because a parameter can repeat. Take the first value only when you expect exactly one.
When a value looks encoded twice
Sometimes a value arrives with %2520 where you expect a space: it was encoded twice by mistake, not nested on purpose. One decode gives %20, a second gives the space. Fix it where it happens, in the code that encodes an already encoded value, rather than adding a second decode on the reading side. A blanket double decode corrupts every value that legitimately contains a %.
Check where a redirect points
A decoded redirect_uri or next parameter is input from whoever built the link. If your login page sends users wherever it says, an attacker can send a real login link of yours that ends on their own site. This is called an open redirect. Parse the decoded URL and allow only your own hosts:
const ALLOWED_HOSTS = new Set(["shop.example.com"]);
const target = new URL(cart);
if (target.protocol !== "https:" || !ALLOWED_HOSTS.has(target.hostname)) {
throw new Error("redirect not allowed");
}Related tools
Related guides
The same character can be fine in one part of a URL and break another. Here is a reference table of what each JavaScript and Python function does with the characters that matter.
Decoding Base64 is one call in every language, and each call has a trap: atob garbles accents, Buffer accepts junk, and Python rejects missing padding. Here is the correct version of each.
Two formatters can disagree about the same JSON file. Here is how a team picks one, applies it in every editor and before every commit, and makes CI enforce it.