|
| 1 | +id: X402_CORS_EXPOSE_HEADERS |
| 2 | +level: MUST |
| 3 | +spec_section: "06-x402-integration §11.2 (Access-Control-Expose-Headers)" |
| 4 | +threat_id: "n/a (x402 spec compliance / browser agent UX)" |
| 5 | +severity: high |
| 6 | +target: hub |
| 7 | +description: > |
| 8 | + Every response from POST /v1/invoke MUST carry |
| 9 | + Access-Control-Expose-Headers including X-Payment-Response, |
| 10 | + X-Request-Id, Retry-After, and WWW-Authenticate. Browsers strip |
| 11 | + non-default response headers from JS access by default; without |
| 12 | + explicit exposure, browser-based agents using the Fetch API cannot |
| 13 | + read the receipt, correlate the request, back off on facilitator |
| 14 | + delays, or surface the challenge to the user. The CORS preflight |
| 15 | + (OPTIONS) response MUST list the same four headers. |
| 16 | +
|
| 17 | +setup: |
| 18 | + - description: register a fresh agent |
| 19 | + request: |
| 20 | + method: POST |
| 21 | + path: /api/agents/register |
| 22 | + headers: { Content-Type: application/json } |
| 23 | + body: | |
| 24 | + { "name": "conformance-x402-cors-${TS}", "agent_type": "conformance" } |
| 25 | + extract: |
| 26 | + agent_id: "$.agent_id" |
| 27 | + api_key: "$.api_key" |
| 28 | + |
| 29 | +request: |
| 30 | + description: invoke an x402 capability with valid X-Payment, expect CORS expose |
| 31 | + method: POST |
| 32 | + path: /v1/invoke |
| 33 | + headers: |
| 34 | + Content-Type: application/json |
| 35 | + X-Agent-ID: "${agent_id}" |
| 36 | + X-API-Key: "${api_key}" |
| 37 | + X-Payment: "${fixtures.x402.valid_payment_b64}" |
| 38 | + Origin: "https://conformance.jecp.dev" |
| 39 | + body: | |
| 40 | + { "jecp": "1.0", "id": "conformance-x402-cors-${TS}", |
| 41 | + "capability": "jecp-test/x402-echo", "action": "echo", |
| 42 | + "input": { "msg": "cors-expose" } } |
| 43 | +
|
| 44 | +expected: |
| 45 | + status: 200 |
| 46 | + headers: |
| 47 | + Access-Control-Expose-Headers: |
| 48 | + present: true |
| 49 | + contains_all: |
| 50 | + - "X-Payment-Response" |
| 51 | + - "X-Request-Id" |
| 52 | + - "Retry-After" |
| 53 | + - "WWW-Authenticate" |
| 54 | + |
| 55 | +negative_cases: |
| 56 | + - description: CORS preflight (OPTIONS) MUST also advertise the four headers |
| 57 | + request: |
| 58 | + method: OPTIONS |
| 59 | + path: /v1/invoke |
| 60 | + headers: |
| 61 | + Origin: "https://conformance.jecp.dev" |
| 62 | + Access-Control-Request-Method: "POST" |
| 63 | + Access-Control-Request-Headers: "X-Agent-ID, X-API-Key, X-Payment, Content-Type" |
| 64 | + expected: |
| 65 | + status_in: [200, 204] |
| 66 | + headers: |
| 67 | + Access-Control-Expose-Headers: |
| 68 | + present: true |
| 69 | + contains_all: |
| 70 | + - "X-Payment-Response" |
| 71 | + - "X-Request-Id" |
| 72 | + - "Retry-After" |
| 73 | + - "WWW-Authenticate" |
| 74 | + Access-Control-Allow-Headers: |
| 75 | + present: true |
| 76 | + contains: "X-Payment" |
| 77 | + |
| 78 | + - description: 402 challenge MUST also carry expose-headers |
| 79 | + request: |
| 80 | + method: POST |
| 81 | + path: /v1/invoke |
| 82 | + headers: |
| 83 | + Content-Type: application/json |
| 84 | + X-Agent-ID: "${agent_id}" |
| 85 | + X-API-Key: "${api_key}" |
| 86 | + Origin: "https://conformance.jecp.dev" |
| 87 | + body: | |
| 88 | + { "jecp": "1.0", "id": "conformance-x402-cors-402-${TS}", |
| 89 | + "capability": "jecp-test/x402-echo", "action": "echo", |
| 90 | + "input": { "msg": "cors-expose-402" } } |
| 91 | + expected: |
| 92 | + status: 402 |
| 93 | + headers: |
| 94 | + Access-Control-Expose-Headers: |
| 95 | + present: true |
| 96 | + contains: "X-Payment-Response" |
| 97 | + |
| 98 | +introduced_in: v1.1.0 |
| 99 | +references: |
| 100 | + - https://jecp.dev/spec/v1.1/06-x402-integration.md#112-access-control-expose-headers--must-on-every-v1invoke-response |
| 101 | + - https://fetch.spec.whatwg.org/#cors-protocol-exposed-response-header-names |
0 commit comments