safe-url

Guards for user-controllable navigation targets: two pure functions, safeExternalUrl / safeInternalPath (auto-imported). Before assigning any user-controllable value (URL query / backend pass-through field / location.hash) to location.href / location.assign / window.open / navigateTo, it must go through a guard — this is the official implementation of the standards client-security hard rule "user-controllable navigation targets must pass a scheme whitelist", so projects no longer hand-copy the same regex.

The two guards are complementary; pick by "where the target should go":

ScenarioUseWhy
External-link interstitial page (?url= countdown redirect), backend pass-through external jump fieldssafeExternalUrlThe target is an off-site http(s) absolute URL by design
Post-login ?redirect= return, back-to-referrersafeInternalPathThe target must stay on-site; absolute external links get blocked too

Both are pure functions with no IO — callable in SSR and CSR alike.

API

FunctionSignatureDescription
safeExternalUrl(raw: unknown, fallback = '/') => stringOnly allows http:// / https:// absolute URLs (case-insensitive, checked after trim); everything else falls back to fallback
safeInternalPath(raw: unknown, fallback = '/') => stringOnly allows on-site absolute paths starting with a single /; everything else falls back to fallback

safeExternalUrl

Parameters

ParameterTypeDefaultDescription
rawunknown—The navigation target to validate (usually from route.query, type unknown — could be an array or undefined)
fallbackstring'/'The fallback address when validation fails

Return Value

string — the trimmed original value when validation passes; fallback otherwise.

Validation Rules

Passes only when the trimmed value matches ^https?:// case-insensitively; everything below falls back to fallback:

  • Dangerous schemes: javascript: / data: / vbscript: etc. (assigning them to location.href executes them → DOM XSS)
  • Protocol-relative URLs (//evil.com), relative paths
  • Empty values, non-strings (e.g. query arrays)

Examples

// Example 1: external-link interstitial page (?url= is fully user-controllable)
const url = safeExternalUrl(route.query.url)
location.href = url

// Example 2: custom fallback address
window.open(safeExternalUrl(raw, '/404'))

// Example 3: backend pass-through jump fields must be guarded too
location.href = safeExternalUrl(res.jump_url)
// Invalid inputs always fall back (default: home '/')
safeExternalUrl('javascript:alert(1)') // → '/' (dangerous scheme)
safeExternalUrl('//evil.com') // → '/' (protocol-relative)
safeExternalUrl('foo/bar') // → '/' (relative path)
safeExternalUrl(undefined) // → '/' (empty / non-string)
safeExternalUrl('  HTTPS://example.com/a  ') // → 'HTTPS://example.com/a' (trim + case-insensitive pass)

safeInternalPath

Parameters

ParameterTypeDefaultDescription
rawunknown—The navigation target to validate (usually from route.query, type unknown)
fallbackstring'/'The fallback address when validation fails

Return Value

string — the trimmed original value when validation passes; fallback otherwise.

Validation Rules

The trimmed value must start with a single / (regex /^\/(?![/\\])/) to pass, e.g. /mine, /movie/detail/1?tab=a; everything below falls back to fallback:

  • Protocol-relative URLs (//evil.com), backslash bypass (/\evil.com) → open redirect
  • Absolute external links (https://evil.com), dangerous schemes
  • Relative / bare paths, empty values, non-strings (including query arrays)

Examples

// Example 1: post-login return (?redirect= is user-controllable and must stay on-site)
await navigateTo(safeInternalPath(route.query.redirect))

// Example 2: back to the referring page (referrer is user-controllable too), custom fallback to /mine
router.push(safeInternalPath(fromPath, '/mine'))

// Example 3: invalid inputs always fall back
safeInternalPath('//evil.com') // → '/' (protocol-relative)
safeInternalPath('/\\evil.com') // → '/' (backslash bypass)
safeInternalPath('https://evil.com') // → '/' (absolute external link — always blocked for on-site returns)
safeInternalPath(undefined) // → '/' (empty / non-string)
safeInternalPath('/movie/detail/1?tab=a') // → '/movie/detail/1?tab=a' (passes)

Attack Vectors

Targets of external-link interstitials, share returns, and post-login ?redirect= come 100% from parameters and are 100% attacker-constructible. Vectors the guards block:

Attack vectorMalicious input exampleConsequence without a guardGuard behavior
Dangerous scheme?url=javascript:alert(1)Assigning to location.href executes the script (DOM XSS); same for data: / vbscript:Both functions fall back to fallback
Protocol-relative URL?url=//evil.comInherits the current protocol and jumps off-site (open redirect)Both functions fall back to fallback
Backslash bypass?redirect=/\evil.comBrowsers normalize \ to /, making it equivalent to //evil.com — off-site jumpsafeInternalPath falls back to fallback
Absolute external link in an on-site return?redirect=https://evil.com/phishThe user is taken to a phishing site after logging insafeInternalPath falls back to fallback

⚠️ Whitelisting is the right answer — don't patch with blacklists. Blacklist-style fixes like "prepend https: when it starts with //" can't withstand encoding bypasses and new variants; both functions are whitelist checks (only explicitly legal shapes pass), everything else falls back.

Notes

  • Both functions are auto-imported; you can also import explicitly: import { safeExternalUrl, safeInternalPath } from '@swift-web/framework'.
  • Pure functions with no IO — callable in SSR and CSR alike (no onMounted / IS_CLIENT guard needed).
  • The fallback value itself is not validated — pass a hard-coded safe constant (like '/' or '/404'), never another user-controllable value.