Harden OAuth next path validation#243
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Tightens GitHub OAuth next redirect handling to avoid ambiguous/unsafe redirects and adds regression tests around open-redirect and header-injection style payloads.
Changes:
- Hardened
_safe_next_pathto reject backslashes and ASCII control characters. - Added unit coverage for
_safe_next_pathbehavior across common malicious inputs. - Added an integration-style test ensuring
/auth/github/loginstores a sanitizednextvalue in OAuth state.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| app/main.py | Expands _safe_next_path validation to reject redirect-ambiguity characters. |
| tests/test_security.py | Adds parametric unit tests for _safe_next_path against external/header-like paths. |
| tests/test_wallet_api.py | Adds regression tests for OAuth login flow and state encoding with unsafe next. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ( | ||
| not next_path | ||
| or not next_path.startswith("/") | ||
| or next_path.startswith("//") | ||
| or "\\" in next_path | ||
| or any(ord(char) < 32 or ord(char) == 127 for char in next_path) | ||
| ): | ||
| return "/me" | ||
| return next_path |
| or not next_path.startswith("/") | ||
| or next_path.startswith("//") | ||
| or "\\" in next_path | ||
| or any(ord(char) < 32 or ord(char) == 127 for char in next_path) |
| @pytest.mark.parametrize( | ||
| "next_path", | ||
| ("//evil.example/path", "/\\evil.example/path", "/me\nLocation:https://evil.example"), | ||
| ) | ||
| def test_oauth_next_path_rejects_redirect_ambiguity(next_path: str) -> None: | ||
| assert _safe_next_path(next_path) == "/me" | ||
|
|
||
|
|
9963092 to
0ffde2b
Compare
|
Addressed the automated review notes in the updated branch:
CI is passing on the updated head. |
prettyboyvic
left a comment
There was a problem hiding this comment.
No blockers found. The change keeps _safe_next_path() narrow at the OAuth state boundary: unsafe external, protocol-relative, backslash, control-character, C1-control, and oversized next values fall back to /me, while normal in-app relative paths still pass through.
Evidence checked:
- Inspected
app/main.py: the guard now rejects empty/non-slash values,//protocol-relative values, backslashes, paths over 2048 chars, ASCII controls, and C1 controls before the signed OAuth state is created. - Inspected
tests/test_security.py: the helper coverage includes external URLs, protocol-relative URLs, backslash ambiguity, newline/header-like input, DEL/C1 controls, oversized values, and valid/meplus/bounties?status=openpaths. - Inspected
tests/test_wallet_api.py: the login-route regression verifies/auth/github/login?next=/%5Cevil.example/pathstores/mein the signed OAuth state. - Checked hosted GitHub CI:
Quality, readiness, docs, and image checksis green and merge state is clean.
Local validation on PR head c6df4d3:
uv run --python 3.12 --extra dev python -m pytest tests/test_security.py::test_oauth_next_path_rejects_external_or_headerlike_paths tests/test_wallet_api.py::test_oauth_next_path_rejects_redirect_ambiguity tests/test_wallet_api.py::test_github_login_stores_safe_default_for_backslash_next -q->15 passeduv run --python 3.12 --extra dev ruff check app/main.py tests/test_security.py tests/test_wallet_api.py-> passeduv run --python 3.12 --extra dev ruff format --check app/main.py tests/test_security.py tests/test_wallet_api.py-> already formattedgit diff --check origin/main...HEAD-> clean
No secrets, wallet material, private deployment values, private vulnerability details, PayPal details, or MRWK price claims are included.
ayskobtw-lil
left a comment
There was a problem hiding this comment.
No blockers from my review.
Evidence checked:
- Inspected
app/main.py;_safe_next_path()now rejects missing/non-relative values, protocol-relative URLs, backslashes, oversized paths, ASCII controls, and C1 controls before the value is signed into OAuth state. - Confirmed the callback still re-runs
_safe_next_path()after verifying state, so both login-time and callback-time handling use the same allow/fallback rule. - Reviewed
tests/test_security.py; helper coverage includes external URLs, protocol-relative URLs, backslash ambiguity, newline/header-like input, C1 controls, DEL, oversized input, and valid in-app paths. - Reviewed
tests/test_wallet_api.py; route-level coverage confirms/auth/github/login?next=/%5Cevil.example/pathstores/mein signed state while the normal configured OAuth redirect path still works.
Validation run on current head c6df4d3:
python -m pytest tests/test_security.py::test_oauth_next_path_rejects_external_or_headerlike_paths tests/test_wallet_api.py::test_oauth_next_path_rejects_redirect_ambiguity tests/test_wallet_api.py::test_github_login_stores_safe_default_for_backslash_next tests/test_wallet_api.py::test_github_login_redirects_when_oauth_is_configured -q->16 passedpython -m ruff check app/main.py tests/test_security.py tests/test_wallet_api.py->All checks passed!git diff --check origin/main...HEAD-> clean
/claim #228
Summary
_safe_next_path()so OAuthnextpaths fall back to/mewhen they are empty, external, protocol-relative, overlong, contain backslashes, or contain control characters./meand/bounties?status=open./auth/github/loginstoring a sanitized fallback in the signed OAuth state.Repro Before Fix
_safe_next_path("/\\evil.example/path")returned the ambiguous path unchanged./auth/github/login?next=/%5Cevil.example/pathstored that ambiguous path in the signed OAuth state, so the callback would later redirect to it after successful GitHub OAuth.Verification
uv run --python 3.12 --extra dev python -m pytest tests/test_security.py::test_oauth_next_path_rejects_external_or_headerlike_paths tests/test_wallet_api.py::test_oauth_next_path_rejects_redirect_ambiguity tests/test_wallet_api.py::test_github_login_stores_safe_default_for_backslash_next -q-> 15 passeduv run --python 3.12 --extra dev python -m pytest -q-> 220 passed, 2 warningsuv run --python 3.12 --extra dev ruff check .-> all checks passeduv run --python 3.12 --extra dev ruff format --check .-> 37 files already formatteduv run --python 3.12 --extra dev python -m mypy app-> successuv run --python 3.12 --extra dev python scripts/docs_smoke.py-> docs smoke okgit diff --check-> cleanNo secrets, wallet material, private vulnerability details, deployment values, payout details, PayPal details, or MRWK price claims are included.