ASVS 5.0 V1.2.2 (Level 1) requires two things when you build URLs from untrusted data: encode each value for the URL part it goes into (URL encoding or base64url for path and query parameters), and allow only safe protocols, so javascript: and data: never end up in a link or redirect.
Why it matters
A value that contains &, # or / can add parameters, truncate a URL or move a path. Without encoding, a search term like a&admin=true quietly becomes two parameters. And if a user supplied URL is rendered as an href, then javascript:alert(document.cookie) runs script on click. Redirect and profile website fields are the usual victims.
How to implement it
Encode each component separately, with a library, never by string concatenation of raw values.
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', userQuery); // encodes for you
url.pathname = '/users/' + encodeURIComponent(userId);
from urllib.parse import quote, urlencode
path = "/users/" + quote(user_id, safe="")
query = urlencode({"q": user_query})
For URLs supplied by users, parse them and check the scheme against an allowlist:
function safeHttpUrl(input) {
try {
const u = new URL(input);
return (u.protocol === 'https:' || u.protocol === 'http:') ? u.toString() : null;
} catch {
return null;
}
}
Render the link only when the function returns a value. Checking that a string starts with http is not enough, since httpx: or whitespace and control character tricks can slip past naive checks, while the URL parser normalizes them.
How to test it
Submit these into every field that becomes a link or redirect target: javascript:alert(1), JaVaScRiPt:alert(1), data:text/html,hello, javascript:alert(1) with a leading space, and //evil.example. None may appear as a working href. For parameters, submit a&b=c#d and confirm the outgoing URL contains a%26b%3Dc%23d.
Common mistakes
- Blocking only the lowercase string
javascript:. - Encoding the whole URL, which breaks the slashes, instead of each value.
- Using
encodeURIfor a parameter value. It leaves&and=alone. - Allowing relative redirects that start with two slashes.
Where this goes wrong in practice
Open redirects are the most common real world case. A login page takes ?next=/account and sends the user there after sign in. An attacker changes it to //evil.example or https://evil.example and shares the link, and the user lands on a fake page that looks trusted because the first click was on your domain. Treat the redirect target the same way as any other URL input: parse it, accept only relative paths that start with a single slash, or compare the host against a short allowlist.
Another frequent case is API clients that build request URLs from IDs. If the ID is ../admin, a path like /users/ plus the ID reaches a different endpoint. Encoding the ID with encodeURIComponent turns the slashes into %2F, which keeps it in one path segment. Add a check on the shape of the ID as well, because encoding stops structure changes but does not prove the value is sensible.
Quick check
Which function encodes a single query value in JavaScript?
encodeURIComponent, or better, URLSearchParams through the URL object. encodeURI is meant for whole URLs and leaves & untouched.