Skip to content

fix(solana): async re-sign payment guard + poll_url host pinning#20

Merged
VickyXAI merged 2 commits into
mainfrom
fix/async-resign-guard
Jul 5, 2026
Merged

fix(solana): async re-sign payment guard + poll_url host pinning#20
VickyXAI merged 2 commits into
mainfrom
fix/async-resign-guard

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

Closes the two follow-ups #19 explicitly deferred in its own commit message.

1. Async mid-poll re-sign payment-terms guard (parity with sync)

The sync poll loop rejects a re-signed 402 challenge whose amount or recipient changed vs. what the job originally authorized (_assert_same_payment_terms). The async path did not — it re-signed whatever the gateway quoted. Now:

  • The async media helper inlines the submit-time signing (instead of _sign_payment_from_response) so it can capture the original amount/pay_to.
  • The async re-sign block mirrors the sync structure exactly: the guard runs outside the try/except, so a re-price PaymentError propagates instead of being swallowed and masked as a generic 402.

2. poll_url host + scheme pinning (SSRF / signed-header exfiltration)

_absolute_url (sync + async) now pins an absolute poll_url to the API origin. The poll loop sends — and now re-signs — the wallet's PAYMENT-SIGNATURE against poll_url; a gateway response redirecting it off-host (or downgrading https→http) would leak the signed payment. Absolute cross-host/scheme URLs are rejected; relative URLs resolve against the API host as before.

Tests (tests/unit/test_solana_media.py)

  • End-to-end re-sign through the real poll loop, sync and async: same-terms re-sign completes; a re-price propagates as PaymentError.
  • poll_url host-pin: relative resolved, same-host passes, cross-host rejected, http-downgrade rejected.

Validation

black + ruff clean; 294 passed (pytest tests/unit). No new mypy errors vs main (pre-existing .json()/price patterns only).

1bcMax added 2 commits July 4, 2026 22:52
Closes the two follow-ups flagged in #19:

- Async mid-poll re-sign now runs the same _assert_same_payment_terms guard
  as the sync path. The async media helper inlines the submit-time signing so
  it can capture the original amount/pay_to, and the re-sign block mirrors the
  sync structure (guard runs OUTSIDE the try/except so a re-price PaymentError
  propagates instead of being masked as a generic 402).
- _absolute_url now host+scheme-pins an absolute poll_url to the API origin
  (sync + async). The poll loop sends and re-signs the wallet PAYMENT-SIGNATURE
  against poll_url, so a gateway response redirecting it off-host would leak the
  signed payment; reject it.

Tests (tests/unit/test_solana_media.py): end-to-end re-sign through the poll
loop for sync AND async (same-terms completes; re-price propagates), plus
poll_url host-pin cases (relative resolved, same-host ok, cross-host and
http-downgrade rejected).
…>=3.10)

test_solana_media.py (added in #19) had no version guard and isn't in CI's
3.9 ignore list, so on 3.9 — where x402[svm] isn't installed — the autouse
codec-stub fixture's monkeypatch.setattr(...decode_payment_required_header)
raised AttributeError and errored the whole module. This is why main went
red on 3.9 after #19. Add pytest.importorskip("x402"/"solders"), matching
test_solana_timeout_routing.py.
@VickyXAI VickyXAI merged commit 5d07eaa into main Jul 5, 2026
3 checks passed
@VickyXAI VickyXAI deleted the fix/async-resign-guard branch July 5, 2026 05:58
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.

1 participant