Skip to content

fix(tools/web): http_request works on Python 3.13; flag https jku/x5u#414

Closed
VoidChecksum wants to merge 1 commit into
mainfrom
fix/web-http-request-asyncio
Closed

fix(tools/web): http_request works on Python 3.13; flag https jku/x5u#414
VoidChecksum wants to merge 1 commit into
mainfrom
fix/web-http-request-asyncio

Conversation

@VoidChecksum
Copy link
Copy Markdown
Collaborator

Summary

Two correctness/security fixes in the web exploitation toolkit, with tests.

1. http_request was dead on Python 3.13 (tools/web/tools.py)

http_request drove its coroutine via asyncio.get_event_loop().run_until_complete(_do()). On the pinned Python 3.13 runtime this raises in both reachable contexts:

  • no current loop → RuntimeError: There is no current event loop
  • inside the agent's running loop → RuntimeError: This event loop is already running

So every call returned an opaque {"error": "RuntimeError ..."} — the tool was unusable on the supported runtime.

Fix: a small _run_coro helper that runs a coroutine safely regardless of context — asyncio.run directly when no loop is running, otherwise driven on a fresh single-worker thread that owns its own loop (ThreadPoolExecutor + asyncio.run). http_request now calls the helper and keeps its existing try/except + return shape. It was the only tool in the module using the broken pattern.

2. JWT jku/x5u key-confusion under-reported (tools/web/jwt.py)

The jku check only fired when jku did not start with https://, so the dominant attack — jku=https://attacker.com/jwks.json — was treated as safe, and x5u was parsed but never checked. The referenced host is attacker-influenced regardless of scheme.

Fix: emit a finding whenever jku or x5u is present, and add the symmetric x5u check. Existing finding-text style preserved.

Tests (tests/unit/web/)

  • test_tools.py: http_request returns a parsed result without a RuntimeError from (a) a plain sync context and (b) inside a running event loop, plus a direct _run_coro guard asserting it works while a loop is active. Outbound HTTP is mocked by patching _get_session.
  • test_jwt.py: a token with jku=https://attacker.com/jwks.json yields a finding; a token with x5u=https://attacker.com/cert.pem yields a finding.

Verification

  • ruff check (4 files): All checks passed
  • ruff format --check (4 files): already formatted
  • basedpyright (both source files): 0 errors (2 pre-existing warnings in http_history, unrelated)
  • pytest packages/decepticon/tests/unit/web -q: 42 passed

http_request drove its coroutine via
`asyncio.get_event_loop().run_until_complete(_do())`. On the pinned
Python 3.13 runtime this raises in both reachable contexts: with no
current loop it errors ("There is no current event loop"), and inside
the agent's running loop it errors ("This event loop is already
running"). Every call therefore returned an opaque RuntimeError, making
the tool dead on the supported runtime.

Introduce a `_run_coro` helper that runs a coroutine safely regardless
of context: it uses `asyncio.run` directly when no loop is running, and
otherwise drives the coroutine on a fresh single-worker thread that owns
its own loop (via ThreadPoolExecutor + asyncio.run). http_request now
calls this helper, preserving the existing try/except and return shape.
http_request is the only tool in this module using the broken pattern.

The JWT jku key-confusion check only fired when `jku` did not start with
`https://`, so the dominant attack — `jku=https://attacker.com/jwks.json`
— was treated as safe, and `x5u` was parsed but never checked. The host
is attacker-influenced regardless of scheme, so emit a finding whenever
`jku` or `x5u` is present, and add the symmetric `x5u` check.

Tests: http_request returns a parsed result without a RuntimeError from
both a plain sync context and from inside a running event loop (and a
direct `_run_coro` guard while a loop is active); JWT parsing flags both
`https` jku and `https` x5u headers.

@pytest.fixture
def patched_session(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(tools, "_get_session", lambda: _FakeSession())
VoidChecksum added a commit that referenced this pull request May 30, 2026
Folds the unique JWT enhancement from #414 into this PR: flag ANY jku header (not only non-HTTPS — an attacker-influenced https JWKS URL is still a key-confusion vector) and add the previously-missing x5u (X.509 URL) check. Uses the string-safe coercion from this PR's non-string-header fix so non-string jku/x5u values still can't crash parse_token. Adds regression tests. Consolidates #414's jwt change; #414's http_request part is already covered by #358.
@VoidChecksum
Copy link
Copy Markdown
Collaborator Author

Closing and consolidating. This PR mixed two concerns:

The CodeQL 'unnecessary lambda' alert that was blocking this PR was in the http_request test, which is dropped. Merge #358 (http_request) + #390 (jwt) instead.

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.

2 participants