Skip to content

Commit 7183906

Browse files
committed
validation added to both go and ts implementations
1 parent 1f39a38 commit 7183906

File tree

6 files changed

+746
-20
lines changed

6 files changed

+746
-20
lines changed

go/http/server.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -473,27 +473,15 @@ func (s *x402HTTPResourceServer) extractPaymentV2(adapter HTTPAdapter) (*types.P
473473
return nil, nil // No payment header
474474
}
475475

476-
// Decode base64 header
477-
jsonBytes, err := decodeBase64Header(header)
476+
// Validate and decode payment header with comprehensive validation
477+
payload, err := ValidateAndDecodePaymentHeader(header)
478478
if err != nil {
479-
return nil, fmt.Errorf("failed to decode payment header: %w", err)
480-
}
481-
482-
// Detect version
483-
version, err := types.DetectVersion(jsonBytes)
484-
if err != nil {
485-
return nil, fmt.Errorf("failed to detect version: %w", err)
479+
return nil, err
486480
}
487481

488482
// V2 server only accepts V2 payments
489-
if version != 2 {
490-
return nil, fmt.Errorf("only V2 payments supported, got V%d", version)
491-
}
492-
493-
// Unmarshal to V2 payload
494-
payload, err := types.ToPaymentPayload(jsonBytes)
495-
if err != nil {
496-
return nil, fmt.Errorf("failed to unmarshal V2 payload: %w", err)
483+
if payload.X402Version != 2 {
484+
return nil, fmt.Errorf("only V2 payments supported, got V%d", payload.X402Version)
497485
}
498486

499487
return payload, nil

go/http/validation.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package http
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"fmt"
7+
"regexp"
8+
9+
"github.com/coinbase/x402/go/types"
10+
)
11+
12+
// Base64 regex pattern - requires at least one character
13+
var base64Regex = regexp.MustCompile(`^[A-Za-z0-9+/]+={0,2}$`)
14+
15+
// ValidateAndDecodePaymentHeader validates and decodes a payment header string.
16+
// It performs comprehensive validation of:
17+
// - Base64 format
18+
// - JSON structure
19+
// - Required fields and their types
20+
//
21+
// Returns the decoded PaymentPayload if valid, or an error with a descriptive message.
22+
func ValidateAndDecodePaymentHeader(paymentHeader string) (*types.PaymentPayload, error) {
23+
// Validate header is not empty
24+
if paymentHeader == "" {
25+
return nil, fmt.Errorf("payment header is empty")
26+
}
27+
28+
// Validate base64 format
29+
if !base64Regex.MatchString(paymentHeader) {
30+
return nil, fmt.Errorf("invalid payment header format: not valid base64")
31+
}
32+
33+
// Decode base64
34+
decoded, err := base64.StdEncoding.DecodeString(paymentHeader)
35+
if err != nil {
36+
return nil, fmt.Errorf("invalid payment header format: base64 decoding failed - %v", err)
37+
}
38+
39+
// Parse JSON into a map first for validation
40+
var rawPayload map[string]interface{}
41+
if err := json.Unmarshal(decoded, &rawPayload); err != nil {
42+
return nil, fmt.Errorf("invalid payment header format: not valid JSON - %v", err)
43+
}
44+
45+
// Validate required top-level fields
46+
if _, exists := rawPayload["x402Version"]; !exists {
47+
return nil, fmt.Errorf("missing required field: x402Version")
48+
}
49+
if version, ok := rawPayload["x402Version"].(float64); !ok {
50+
return nil, fmt.Errorf("invalid field type: x402Version must be a number")
51+
} else if int(version) < 1 {
52+
return nil, fmt.Errorf("invalid value: x402Version must be at least 1")
53+
}
54+
55+
if _, exists := rawPayload["resource"]; !exists {
56+
return nil, fmt.Errorf("missing required field: resource")
57+
}
58+
resourceMap, ok := rawPayload["resource"].(map[string]interface{})
59+
if !ok {
60+
return nil, fmt.Errorf("invalid field type: resource must be an object")
61+
}
62+
63+
// Validate resource fields
64+
if _, exists := resourceMap["url"]; !exists {
65+
return nil, fmt.Errorf("missing required field: resource.url")
66+
}
67+
if _, ok := resourceMap["url"].(string); !ok {
68+
return nil, fmt.Errorf("invalid field type: resource.url must be a string")
69+
}
70+
71+
if _, exists := resourceMap["description"]; !exists {
72+
return nil, fmt.Errorf("missing required field: resource.description")
73+
}
74+
if _, ok := resourceMap["description"].(string); !ok {
75+
return nil, fmt.Errorf("invalid field type: resource.description must be a string")
76+
}
77+
78+
if _, exists := resourceMap["mimeType"]; !exists {
79+
return nil, fmt.Errorf("missing required field: resource.mimeType")
80+
}
81+
if _, ok := resourceMap["mimeType"].(string); !ok {
82+
return nil, fmt.Errorf("invalid field type: resource.mimeType must be a string")
83+
}
84+
85+
if _, exists := rawPayload["accepted"]; !exists {
86+
return nil, fmt.Errorf("missing required field: accepted")
87+
}
88+
if _, ok := rawPayload["accepted"].(map[string]interface{}); !ok {
89+
return nil, fmt.Errorf("invalid field type: accepted must be an object")
90+
}
91+
92+
if _, exists := rawPayload["payload"]; !exists {
93+
return nil, fmt.Errorf("missing required field: payload")
94+
}
95+
if _, ok := rawPayload["payload"].(map[string]interface{}); !ok {
96+
return nil, fmt.Errorf("invalid field type: payload must be an object")
97+
}
98+
99+
// If all validations pass, unmarshal into the PaymentPayload struct
100+
var payload types.PaymentPayload
101+
if err := json.Unmarshal(decoded, &payload); err != nil {
102+
return nil, fmt.Errorf("failed to parse payment payload: %v", err)
103+
}
104+
105+
return &payload, nil
106+
}

0 commit comments

Comments
 (0)