@@ -267,6 +267,45 @@ async def _request_human_approval(
267267 return True , "auto-approved (no approval webhook configured)"
268268
269269
270+ async def _scan_and_validate (page_url : str , action_label : str = "Payment" ) -> tuple [bool , str ]:
271+ """Run security scan on page_url. Returns (ok, message).
272+
273+ ok=True means scan passed (or was skipped). ok=False means the caller
274+ should return `message` immediately as a rejection.
275+ """
276+ if not page_url :
277+ return True , f" (security scan skipped — no page_url provided)"
278+
279+ # Check cache first (reuse recent scan within 5 minutes)
280+ cached = snapshot_cache .get (page_url )
281+ if cached and datetime .now () - cached ["timestamp" ] < timedelta (minutes = 5 ):
282+ scan_result = {
283+ "flags" : cached ["flags" ],
284+ "snapshot_id" : cached ["snapshot_id" ],
285+ "safe" : "hidden_instructions_detected" not in cached ["flags" ],
286+ "error" : None ,
287+ }
288+ else :
289+ scan_result = await _scan_page (page_url )
290+
291+ if scan_result .get ("error" ):
292+ return False , (
293+ f"{ action_label } rejected. Security scan failed: { scan_result ['error' ]} "
294+ f"Snapshot ID: { scan_result ['snapshot_id' ]} . "
295+ f"Fix the URL or skip page_url if the page has no associated URL."
296+ )
297+
298+ if not scan_result ["safe" ]:
299+ return False , (
300+ f"{ action_label } rejected. Security scan detected hidden prompt injection. "
301+ f"Snapshot ID: { scan_result ['snapshot_id' ]} . "
302+ f"Flags: { scan_result ['flags' ]} . "
303+ f"Do not retry this."
304+ )
305+
306+ return True , ""
307+
308+
270309# ---------------------------------------------------------------------------
271310# MCP Tools
272311# ---------------------------------------------------------------------------
@@ -303,37 +342,10 @@ async def request_virtual_card(
303342 # -------------------------------------------------------------------
304343 # P1: Automatic security scan (runs whenever page_url is provided)
305344 # -------------------------------------------------------------------
306- scan_note = ""
307- if page_url :
308- # Check cache first (reuse recent scan within 5 minutes)
309- cached = snapshot_cache .get (page_url )
310- if cached and datetime .now () - cached ["timestamp" ] < timedelta (minutes = 5 ):
311- scan_result = {
312- "flags" : cached ["flags" ],
313- "snapshot_id" : cached ["snapshot_id" ],
314- "safe" : "hidden_instructions_detected" not in cached ["flags" ],
315- "error" : None ,
316- }
317- else :
318- scan_result = await _scan_page (page_url )
319-
320- if scan_result .get ("error" ):
321- # Network/URL error — treat as unsafe; do not issue card
322- return (
323- f"Payment rejected. Security scan failed: { scan_result ['error' ]} "
324- f"Snapshot ID: { scan_result ['snapshot_id' ]} . "
325- f"Fix the URL or skip page_url if the checkout has no associated URL."
326- )
327-
328- if not scan_result ["safe" ]:
329- return (
330- f"Payment rejected. Security scan detected hidden prompt injection. "
331- f"Snapshot ID: { scan_result ['snapshot_id' ]} . "
332- f"Flags: { scan_result ['flags' ]} . "
333- f"Do not retry this payment."
334- )
335- else :
336- scan_note = " (security scan skipped — no page_url provided)"
345+ scan_ok , scan_msg = await _scan_and_validate (page_url , action_label = "Payment" )
346+ if not scan_ok :
347+ return scan_msg
348+ scan_note = scan_msg # empty string when scan passed, skip note when no page_url
337349
338350 # Human approval gate (if POP_APPROVAL_WEBHOOK is configured)
339351 require_approval = os .getenv ("POP_REQUIRE_HUMAN_APPROVAL" , "false" ).lower () == "true"
@@ -503,34 +515,9 @@ async def request_purchaser_info(
503515 # -------------------------------------------------------------------
504516 # P1: Automatic security scan (runs whenever page_url is provided)
505517 # -------------------------------------------------------------------
506- if page_url :
507- # Check cache first (reuse recent scan within 5 minutes)
508- cached = snapshot_cache .get (page_url )
509- if cached and datetime .now () - cached ["timestamp" ] < timedelta (minutes = 5 ):
510- scan_result = {
511- "flags" : cached ["flags" ],
512- "snapshot_id" : cached ["snapshot_id" ],
513- "safe" : "hidden_instructions_detected" not in cached ["flags" ],
514- "error" : None ,
515- }
516- else :
517- scan_result = await _scan_page (page_url )
518-
519- if scan_result .get ("error" ):
520- # Network/URL error — treat as unsafe; do not inject info
521- return (
522- f"Billing info rejected. Security scan failed: { scan_result ['error' ]} "
523- f"Snapshot ID: { scan_result ['snapshot_id' ]} . "
524- f"Fix the URL or skip page_url if the page has no associated URL."
525- )
526-
527- if not scan_result ["safe" ]:
528- return (
529- f"Billing info rejected. Security scan detected hidden prompt injection. "
530- f"Snapshot ID: { scan_result ['snapshot_id' ]} . "
531- f"Flags: { scan_result ['flags' ]} . "
532- f"Do not retry this."
533- )
518+ scan_ok , scan_msg = await _scan_and_validate (page_url , action_label = "Billing info" )
519+ if not scan_ok :
520+ return scan_msg
534521
535522 if injector is None :
536523 return (
0 commit comments