Skip to content

Refuse the OVH VPS fleet and charge spoofed browsers (x402-gateway 0.2.2) - #547

Merged
ralyodio merged 1 commit into
masterfrom
edge-controls
Sep 5, 2026
Merged

Refuse the OVH VPS fleet and charge spoofed browsers (x402-gateway 0.2.2)#547
ralyodio merged 1 commit into
masterfrom
edge-controls

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What changes

@profullstack/x402-gateway 0.1.0 → 0.2.2 and its three edge controls switched on in src/lib/crawl-gateway.ts. The lists from #545 only catch crawlers that name themselves; this handles the ones that do not.

Request Before After
From an OVH VPS range (51.38/16, 54.38/16, 141.94/16, 145.239/16, 149.202/16, 151.80/16, 57.129/16, 213.32/16) served 403 Not available from this network. before anything else runs, from the edge's last hop of x-forwarded-for (or x-real-ip), so a client seeding the header cannot step around it
Chrome/… user agent with no Sec-Fetch-Mode header (every Chromium since 76 sends it; it is a forbidden header no page script or extension can remove) served 402 with the x402 offer, same as GPTBot. Only Chromium claims are judged; Firefox and Safari are untouched
Googlebot's evergreen UA (… compatible; Googlebot/2.1 … Chrome/145 …), Bingbot, anything declaring itself served served, unchanged: declared bots are judged by the lists, never by the spoof check
Any request with a Supabase session cookie (sb-<project-ref>-auth-token, chunked .0/.1) served served, never charged, whatever the client looks like. The pre-sign-in -auth-token-code-verifier cookie does not count
Real browsers, declared retrieval crawlers, /crawl, robots.txt as in #545 as in #545

The OVH ranges are the VPS fleet (vps-*.vps.ovh.net) measured 2026-08-28 on rssamplifier, crawling with a spoofed Chrome/148 string; nobody reads ugig from an OVH VPS. Order inside the gateway: CIDR deny → /crawl → exempt (session) → lists or spoof check → open paths → pass header → 402.

Cookie matching

The app's Supabase middleware (@profullstack/stack/supabase@supabase/ssr createServerClient) passes no cookieOptions.name, so the session cookie is the library default sb-<ref>-auth-token, chunked when large. hasSupabaseSession matches sb-*-auth-token optionally followed by .N, then =.

Tests (src/lib/crawl-gateway.test.ts, 18 tests)

New: OVH last-hop x-forwarded-for → 403; OVH seeded as the first hop → passes (denylist reads the edge's hop); Chrome UA without sec-fetch-mode on /gigs/x → 402; with sec-fetch-mode: navigate → passes; Chrome without sec-fetch but with sb-xxx-auth-token= (and the chunked form) → passes; the cookie matcher rejects -code-verifier and unrelated cookies; Googlebot evergreen UA → passes. The existing Chrome pass-through and proxy composition cases now send sec-fetch-mode, since a bare Chrome string is charged on purpose.

Checks

  • pnpm type-check: clean
  • pnpm lint: 0 errors (40 pre-existing warnings, none in the changed files)
  • pnpm test:run: 215 files, 2051 tests passed
  • pnpm@9 install --frozen-lockfile from a wiped node_modules: passes; lockfile diff is the five lines for the version bump (0.2.2 has no dependencies)

Worth knowing

  • A privacy proxy that strips Sec-Fetch-* from a real Chrome user would see a 402 on public pages while signed out; signing in exempts them. Sec-Fetch-Mode was chosen over sec-ch-ua for exactly this reason (it survives more proxies), but it is not zero.
  • A CDN in front of the edge would make the last hop the CDN's and the denylist would fail safe (nothing refused). There is none today (Railway's proxy is the edge).
  • Not merged; COINPAY_X402_KEY and CRAWL_PAY_TO still gate whether the 402 carries a price.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJaXiqE9BDoNfoJBhfXroC

…2.2)

The lists only catch crawlers that name themselves. Two on this site do
not: an OVH VPS fleet (vps-*.vps.ovh.net, measured 2026-08-28 on
rssamplifier) wearing "Chrome/148", and rotations cycling a few Chrome
strings across many addresses. 0.2.2 adds the two edge controls for them,
switched on here:

- denyCidrs: eight OVH /16s answered with a tiny 403 before anything else,
  read from the edge's own last hop of x-forwarded-for (or x-real-ip), so a
  client seeding the header cannot step around it.
- chargeSpoofedBrowsers: a "Chrome/..." UA without Sec-Fetch-Mode, a header
  every Chromium sends and no script can strip, is charged like GPTBot.
  Googlebot's evergreen Chrome string, Bingbot and anything "compatible;"
  are judged by the lists, never by this.
- exempt: a request carrying a Supabase session cookie
  (sb-<ref>-auth-token, chunked .0/.1; not the pre-sign-in code-verifier)
  is never charged, whatever the client looks like.

Tests cover each: OVH last hop 403, seeded first hop ignored, Chrome
without sec-fetch-mode 402, with it passes, session cookie passes,
Googlebot evergreen passes, and the cookie matcher's edges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJaXiqE9BDoNfoJBhfXroC
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

45 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:79
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:90
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:113
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:144
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:137
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-api-key docs/agents/integration-guide.md:893
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:158
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:182
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:193
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:232
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
LOW secret-generic-credential src/lib/validations.test.ts:148
LOW secret-generic-credential src/lib/validations.test.ts:512
LOW secret-generic-credential src/lib/validations.test.ts:523
LOW secret-generic-credential src/lib/validations.test.ts:538
LOW secret-generic-credential src/lib/validations.test.ts:548
LOW secret-generic-credential src/lib/validations.test.ts:557
LOW secret-generic-credential src/lib/validations.test.ts:567
LOW secret-generic-credential src/lib/validations.test.ts:582

Snippets are redacted; ThreatCrush never prints matched credential material.

@socket-security

Copy link
Copy Markdown
Contributor

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​profullstack/​x402-gateway@​0.1.0 ⏵ 0.2.276 +1100100 +188100

View full report

@ralyodio
ralyodio merged commit 11d758e into master Sep 5, 2026
6 checks passed
@ralyodio
ralyodio deleted the edge-controls branch September 5, 2026 17:32
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