diff --git a/.agents/configs/prompt-injection-patterns.yaml b/.agents/configs/prompt-injection-patterns.yaml index c1a20728b3..b66e893d13 100644 --- a/.agents/configs/prompt-injection-patterns.yaml +++ b/.agents/configs/prompt-injection-patterns.yaml @@ -24,6 +24,7 @@ # social_engineering — Urgency, authority claims, emotional manipulation # data_exfiltration — Attempts to leak data via URLs/encoding # data_exfiltration_dns — DNS-based data exfiltration (CVE-2025-55284) +# credential_exposure — Secrets embedded in URLs/config output (t4954) # context_manipulation — Token smuggling, history manipulation, false claims # # ADDING PATTERNS: @@ -614,3 +615,43 @@ context_manipulation: - severity: HIGH description: "Dismisses previous content as fake" pattern: '(?i)\bthe\s+above\s+(was|is)\s+(just\s+)?(a\s+)?(test|joke|fake|distraction)' + +# ================================================================ +# CREDENTIAL EXPOSURE (t4954) +# ================================================================ +# Detects secrets embedded in URL query parameters within command output. +# These patterns catch credential material that leaks from application +# config tables (webhook settings, OAuth configs, integration records) +# where authenticated callback URLs contain secrets as query params. +credential_exposure: + - severity: MEDIUM + description: "URL query param: secret" + pattern: '[?&]secret=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: token" + pattern: '[?&]token=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: api_key/apikey" + pattern: '[?&](api_key|apikey|api-key)=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: password" + pattern: '[?&]password=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: access_token" + pattern: '[?&]access_token=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: auth/authorization" + pattern: '[?&](auth|authorization)=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: client_secret" + pattern: '[?&]client_secret=[^&\s]{8,}' + + - severity: MEDIUM + description: "URL query param: webhook_secret" + pattern: '[?&]webhook_secret=[^&\s]{8,}' diff --git a/.agents/prompts/build.txt b/.agents/prompts/build.txt index 77d4462ed6..87d47dbad5 100644 --- a/.agents/prompts/build.txt +++ b/.agents/prompts/build.txt @@ -273,6 +273,28 @@ When referencing specific functions or code include the pattern `file_path:line_ # the secret value before presenting it to the user. - After running any command that references a credential variable, assess whether the output could contain the secret value. If the command failed (non-zero exit) and the secret was passed as an argument (violating 8.2), assume the output is contaminated — do not present it to the user. Flag for immediate credential rotation. - This is a judgment call, not a regex check. The agent knows which variables contain secrets and can assess whether output looks like it contains credential material (long base64 strings, API key patterns, JSON with auth fields). +# +# 8.4 Application config contains embedded credentials (t4954) +# Threat: application configuration tables (webhook settings, integration +# records, OAuth configs, API endpoint metadata) store authenticated callback +# URLs with secrets as query parameters (e.g., `?secret=`). A general +# `SELECT *` or `SELECT value` on these tables returns the full record +# including embedded credentials — even though the command itself doesn't +# reference any credential variable. Sections 8.2 and 8.3 don't catch this +# because the secret isn't passed as an argument or referenced as a variable. +# Incident: FluentForms webhook config queried via `wp db query`, output +# contained `request_url` with `?secret=`. Required immediate rotation. +- When querying application config (webhook settings, integration records, OAuth configs, API endpoint metadata), NEVER fetch raw record values with `SELECT *` or unfiltered column reads. Query schema/keys first, then extract only non-credential fields via targeted selectors (`jq` field filters, `wp eval` with specific property access, SQL column lists excluding URL/token/secret fields). + - UNSAFE: `wp db query "SELECT value FROM wp_fluentform_form_meta WHERE meta_key='fluentform_webhook_feed'"` — returns full JSON including `request_url` with embedded `?secret=` + - UNSAFE: `SELECT * FROM wp_options WHERE option_name LIKE '%webhook%'` — option values often contain authenticated URLs + - UNSAFE: `wp option get ` — raw JSON dump may contain OAuth tokens, API keys, or signed URLs + - SAFE: `wp db query "SELECT meta_key FROM wp_fluentform_form_meta WHERE form_id=1"` — schema/key discovery only, no values + - SAFE: `wp eval 'echo json_encode(array_keys(json_decode(get_option("webhook_config"), true)));'` — key names only + - SAFE: `wp db query "SELECT name, status, form_id FROM wp_fluentform_form_meta WHERE ..."` — specific non-secret columns + - SAFE: pipe raw output through `jq 'del(.request_url, .secret, .token, .api_key)'` to strip credential fields before display +- URLs in config records frequently contain embedded secrets as query parameters (`?secret=`, `?token=`, `?key=`, `?api_key=`, `?password=`). Treat any URL field in application config as potentially containing credentials. +- This applies broadly: WordPress options/meta, Stripe webhook endpoints, Zapier/Make.com integration configs, OAuth redirect URIs with state tokens, any SaaS callback URL stored in a database. +- When investigating webhook or integration issues, describe the config structure (field names, record count, status) without exposing field values. If a specific URL is needed for debugging, ask the user to check it in their admin UI. - Confirm destructive operations before execution - NEVER create files in `~/` root - use `~/.aidevops/.agent-workspace/work/[project]/` - Do not commit files containing secrets (.env, credentials.json, etc.) diff --git a/.agents/scripts/prompt-guard-helper.sh b/.agents/scripts/prompt-guard-helper.sh index 802c26518a..a13642a1e2 100755 --- a/.agents/scripts/prompt-guard-helper.sh +++ b/.agents/scripts/prompt-guard-helper.sh @@ -244,7 +244,7 @@ _pg_load_yaml_patterns() { # data_exfiltration, data_exfiltration_dns, context_manipulation, # homoglyph, unicode_manipulation, fake_role, comment_injection, # priority_manipulation, fake_delimiter, split_personality, -# steganographic, fake_conversation +# steganographic, fake_conversation, credential_exposure # YAML pattern file path (Lasso-compatible format) PROMPT_GUARD_YAML_PATTERNS="${PROMPT_GUARD_YAML_PATTERNS:-}" @@ -327,6 +327,14 @@ LOW|unicode_manipulation|Mixed script with injection|\p{Cyrillic}[\x00-\x7F]*(ns LOW|steganographic|Acrostic instruction pattern|[A-Z][a-z]+\s*\n[A-Z][a-z]+\s*\n[A-Z][a-z]+\s*\n[A-Z][a-z]+\s*\n[A-Z][a-z]+ LOW|system_prompt_extraction|System prompt extraction variant|([Ww]rite|[Tt]ype|[Oo]utput)\s+(out\s+)?(the\s+)?(text|content|words)\s+(above|before|preceding)\s+(this|my)\s+(message|input|prompt) LOW|system_prompt_extraction|Prompt leak via translation|([Tt]ranslate|[Cc]onvert)\s+(your\s+)?(system\s+)?(prompt|instructions|rules)\s+(to|into)\s+(French|Spanish|Chinese|another\s+language) +MEDIUM|credential_exposure|URL query param: secret|[?&]secret=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: token|[?&]token=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: key|[?&](api_key|apikey|api-key)=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: password|[?&]password=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: access_token|[?&]access_token=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: auth|[?&](auth|authorization)=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: client_secret|[?&]client_secret=[^&\s]{8,} +MEDIUM|credential_exposure|URL query param: webhook_secret|[?&]webhook_secret=[^&\s]{8,} PATTERNS return 0 } @@ -579,6 +587,10 @@ _pg_sanitize_message() { # Strip long unicode escape sequences sanitized=$(printf '%s' "$sanitized" | sed -E 's/(\\u[0-9a-fA-F]{4}){4,}/[unicode-filtered]/g') + # Redact credential values in URL query parameters (t4954) + # Matches ?secret=VALUE or &token=VALUE etc. and replaces VALUE with [REDACTED] + sanitized=$(printf '%s' "$sanitized" | sed -E 's/([?&](secret|token|api_key|apikey|api-key|password|access_token|auth|authorization|client_secret|webhook_secret)=)[^&[:space:]]{8,}/\1[REDACTED]/g') + printf '%s' "$sanitized" return 0 } @@ -1479,6 +1491,39 @@ cmd_test() { echo "Testing URL encoded payload (MEDIUM — should WARN):" _test_expect "URL encoded payload" 2 "Check this: %69%67%6E%6F%72%65%20%70%72%65%76%69%6F%75%73" + echo "" + echo "Testing URL credential exposure (MEDIUM — should WARN, t4954):" + _test_expect "URL with ?secret= param" 2 "https://example.com/webhook?secret=abc123def456ghi789" + _test_expect "URL with &token= param" 2 "https://api.example.com/callback?id=1&token=FAKE_SK_LIVE_abcdef123456" + _test_expect "URL with ?api_key= param" 2 "https://hooks.example.com/v1?api_key=FAKE_AKIA_IOSFODNN7EXAMPLE" + _test_expect "URL with ?password= param" 2 "https://service.example.com/auth?password=SuperSecret123!" + _test_expect "URL with ?access_token= param" 2 "https://api.example.com/data?access_token=FAKE_JWT_aGVhZGVyLnBheWxvYWQ" + _test_expect "URL with ?client_secret= param" 2 "https://oauth.example.com/token?client_secret=FAKE_CS_abcdef123456789" + _test_expect "Short param value (no match)" 0 "https://example.com/page?secret=abc" + + echo "" + echo "Testing URL credential sanitization (t4954):" + total=$((total + 1)) + local url_sanitized + url_sanitized=$(PROMPT_GUARD_QUIET="true" cmd_sanitize "Webhook URL: https://example.com/hook?secret=abc123def456ghi789&name=test" 2>/dev/null) + if [[ "$url_sanitized" == *"[REDACTED]"* ]] && [[ "$url_sanitized" != *"abc123def456ghi789"* ]]; then + echo -e " ${GREEN}PASS${NC} URL secret param redacted in sanitization" + passed=$((passed + 1)) + else + echo -e " ${RED}FAIL${NC} URL secret param not redacted: $url_sanitized" + failed=$((failed + 1)) + fi + + total=$((total + 1)) + url_sanitized=$(PROMPT_GUARD_QUIET="true" cmd_sanitize "Config: https://api.example.com/v1?token=sk_live_abcdef123456&format=json" 2>/dev/null) + if [[ "$url_sanitized" == *"[REDACTED]"* ]] && [[ "$url_sanitized" == *"format=json"* ]]; then + echo -e " ${GREEN}PASS${NC} URL token param redacted, non-secret params preserved" + passed=$((passed + 1)) + else + echo -e " ${RED}FAIL${NC} URL token sanitization incorrect: $url_sanitized" + failed=$((failed + 1)) + fi + # ── scan-stdin tests ──────────────────────────────────────── echo ""