Skip to content

fix(indiekit): accept local redirect paths containing -, . and % - #887

Merged
paulrobertlloyd merged 1 commit into
getindiekit:mainfrom
rmdes:fix/redirect-path-validation
Aug 22, 2026
Merged

paulrobertlloyd merged 1 commit into
getindiekit:mainfrom
rmdes:fix/redirect-path-validation

Conversation

@rmdes

@rmdes rmdes commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #886.

IndieAuth.authorize() validates the redirect query parameter with /^\/[\w&/=?]*$/. That class contains no ., - or %, so ordinary local paths are rejected with "Invalid redirect attempted":

  • /auth/new-password
  • /files/upload-photos
  • /posts/2026%2F08

Why not just add the characters

The narrow class is also what stops a protocol-relative URL. //website.example starts with a slash and contains only word characters and a dot, so adding . alone makes it match, and a browser resolves that as https://website.example — an open redirect.

So this keeps the guard on the shape of the value rather than on the character set:

const validRedirect = redirect.match(/^\/(?![/\\])[\w&/=?.\-~:%+@#]*$/);

The negative lookahead rejects a second leading slash, and a backslash, since browsers normalise /\ to //.

Tests

Extends 403-session-auth-invalid-redirect.js:

  • three paths that main rejects today and should not — these fail without the change and pass with it;
  • four off-site forms that must keep returning 403 — //external.example, ///external.example, //user@external.example and /\external.example. These pass on main too; they are there so a later widening of the class cannot quietly reopen the redirect.

node --test in packages/indiekit: 105 tests, 105 passing; 98/98 on main before this change.

`authorize()` validates the `redirect` query parameter against
`/^\/[\w&/=?]*$/`. That character class has no `.`, `-` or `%`, so ordinary
local paths are refused with "Invalid redirect attempted":

    /auth/new-password
    /files/upload-photos
    /posts/2026%2F08

Widening the class alone reopens something the narrow one closed by accident.
`//website.example` starts with a slash and contains only word characters and
a dot, so adding `.` makes it match — and a browser resolves a
protocol-relative URL as an absolute one, turning the check into an open
redirect.

Allow the characters a path may legitimately contain, and reject a second
leading slash (or a backslash, which browsers normalise to a slash) with a
negative lookahead, so the guard tests the shape of the value rather than
relying on a restrictive character set.

Extends the existing integration test: three cases covering paths that main
rejects today, and four covering off-site forms that must keep returning 403
(//example, ///example, //user@example and /\example).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGHR7MuyvBaDbAFfAGUxeT
@paulrobertlloyd
paulrobertlloyd merged commit f326f60 into getindiekit:main Aug 22, 2026
1 check failed
@paulrobertlloyd

paulrobertlloyd commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator

Ah, this might explain an error I keep seeing when I log back into my ‘docked’ web app; I sign in and then get an error page possibly due to attempting to redirect back to a page containing a -! Thanks @rmdes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IndieAuth: local redirect paths containing -, . or % are rejected as invalid

2 participants