Skip to content

Harden OAuth next path validation#243

Merged
ramimbo merged 3 commits into
ramimbo:mainfrom
TUPM96:codex/bounty-228-oauth-next-path
May 25, 2026
Merged

Harden OAuth next path validation#243
ramimbo merged 3 commits into
ramimbo:mainfrom
TUPM96:codex/bounty-228-oauth-next-path

Conversation

@TUPM96
Copy link
Copy Markdown
Contributor

@TUPM96 TUPM96 commented May 25, 2026

/claim #228

Summary

  • Hardened _safe_next_path() so OAuth next paths fall back to /me when they are empty, external, protocol-relative, overlong, contain backslashes, or contain control characters.
  • Preserved normal in-app relative paths such as /me and /bounties?status=open.
  • Added regression coverage for the helper and for /auth/github/login storing 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/path stored 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 passed
  • uv run --python 3.12 --extra dev python -m pytest -q -> 220 passed, 2 warnings
  • uv run --python 3.12 --extra dev ruff check . -> all checks passed
  • uv run --python 3.12 --extra dev ruff format --check . -> 37 files already formatted
  • uv run --python 3.12 --extra dev python -m mypy app -> success
  • uv run --python 3.12 --extra dev python scripts/docs_smoke.py -> docs smoke ok
  • git diff --check -> clean

No secrets, wallet material, private vulnerability details, deployment values, payout details, PayPal details, or MRWK price claims are included.

Copilot AI review requested due to automatic review settings May 25, 2026 08:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_path to reject backslashes and ASCII control characters.
  • Added unit coverage for _safe_next_path behavior across common malicious inputs.
  • Added an integration-style test ensuring /auth/github/login stores a sanitized next value 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.

Comment thread app/main.py
Comment on lines +332 to 340
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
Comment thread app/main.py Outdated
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)
Comment thread tests/test_wallet_api.py
Comment on lines +435 to +442
@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"


@TUPM96 TUPM96 force-pushed the codex/bounty-228-oauth-next-path branch from 9963092 to 0ffde2b Compare May 25, 2026 09:00
@TUPM96
Copy link
Copy Markdown
Contributor Author

TUPM96 commented May 25, 2026

Addressed the automated review notes in the updated branch:

  • Added a 2048-character cap for OAuth next paths.
  • Expanded control-character rejection to include C1 controls (127 <= ord(char) < 160).
  • Kept helper coverage in tests/test_security.py and left tests/test_wallet_api.py focused on the login-route state regression.

CI is passing on the updated head.

Copy link
Copy Markdown
Contributor

@prettyboyvic prettyboyvic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /me plus /bounties?status=open paths.
  • Inspected tests/test_wallet_api.py: the login-route regression verifies /auth/github/login?next=/%5Cevil.example/path stores /me in the signed OAuth state.
  • Checked hosted GitHub CI: Quality, readiness, docs, and image checks is 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 passed
  • uv run --python 3.12 --extra dev ruff check app/main.py tests/test_security.py tests/test_wallet_api.py -> passed
  • uv run --python 3.12 --extra dev ruff format --check app/main.py tests/test_security.py tests/test_wallet_api.py -> already formatted
  • git diff --check origin/main...HEAD -> clean

No secrets, wallet material, private deployment values, private vulnerability details, PayPal details, or MRWK price claims are included.

Copy link
Copy Markdown
Contributor

@ayskobtw-lil ayskobtw-lil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/path stores /me in 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 passed
  • python -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

@ramimbo ramimbo merged commit c671c15 into ramimbo:main May 25, 2026
1 check passed
@ramimbo ramimbo added mrwk:accepted Maintainer accepted for payout mrwk:paid Ledger payment recorded labels May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mrwk:accepted Maintainer accepted for payout mrwk:paid Ledger payment recorded

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants