@@ -8,11 +8,34 @@ import (
88 "html/template"
99 "math/big"
1010 "net/http"
11+ "regexp"
1112 "strings"
1213
1314 x402types "github.com/coinbase/x402/go/types"
1415)
1516
17+ // displayTokenRe is the allowed charset for ServiceOffer-sourced strings
18+ // (offer name, model id) that get interpolated into copy-pasteable CLI
19+ // commands and prompts on the 402 page. It permits the model-id / k8s-name
20+ // vocabulary — alphanumerics plus `. _ : / -` — and nothing else, so shell
21+ // metacharacters can never reach a command a reader might paste.
22+ var displayTokenRe = regexp .MustCompile (`^[A-Za-z0-9._:/-]+$` )
23+
24+ // sanitizeDisplayToken guards a CR-sourced string before it lands in a
25+ // copy-pasteable command on the public 402 page. A ServiceOffer is
26+ // operator-authored, but the page is served over the public tunnel, so a
27+ // hostile or fat-fingered spec.model.name / metadata.name must not smuggle
28+ // shell metacharacters into the rendered command. Anything that isn't a clean
29+ // model-id/k8s-name token (including empty/whitespace) collapses to the
30+ // caller's placeholder.
31+ func sanitizeDisplayToken (s , placeholder string ) string {
32+ s = strings .TrimSpace (s )
33+ if s == "" || ! displayTokenRe .MatchString (s ) {
34+ return placeholder
35+ }
36+ return s
37+ }
38+
1639//go:embed templates/payment_required.html
1740var paymentRequiredHTMLSrc string
1841
@@ -229,7 +252,7 @@ func sendPaymentRequiredHTML(w http.ResponseWriter, r *http.Request, requirement
229252 NetworkLabel : networkLabel ,
230253 PriceDisplay : priceDisplay ,
231254 PayToDisplay : payToDisplay ,
232- PayToFull : payToFull ,
255+ PayToFull : payToFull ,
233256 ExplorerURL : display .ExplorerURL ,
234257 OfferDescription : display .OfferDescription ,
235258 Skills : display .AgentSkills ,
@@ -347,14 +370,8 @@ func normalizeOfferType(t string) string {
347370// raw-JSON paths, but reframed so users understand they're buying remote
348371// model time, not an agent with tools/memory.
349372func inferenceCopy (url string , d PaymentDisplay ) typeCopy {
350- model := strings .TrimSpace (d .Model )
351- if model == "" {
352- model = "<model-id>"
353- }
354- name := strings .TrimSpace (d .OfferName )
355- if name == "" {
356- name = "remote-inference"
357- }
373+ model := sanitizeDisplayToken (d .Model , "<model-id>" )
374+ name := sanitizeDisplayToken (d .OfferName , "remote-inference" )
358375
359376 cmd := fmt .Sprintf (
360377 "obol buy inference %s \\ \n --seller %s \\ \n --model %s \\ \n --budget 1 \\ \n --no-verify-identity" ,
@@ -397,7 +414,7 @@ func inferenceCopy(url string, d PaymentDisplay) typeCopy {
397414// example sits next to the raw x402 JSON in the Pay-manually card to
398415// make the wire shape obvious to readers walking the spec by hand.
399416func agentCopy (url string , d PaymentDisplay ) typeCopy {
400- model := strings . TrimSpace (d .Model )
417+ model := sanitizeDisplayToken (d .Model , "" )
401418 modelClause := ""
402419 modelLine := ""
403420 if model != "" {
0 commit comments