T09 · Insecure Skill Coding Practices
Warning
- Location
- SKILL.md:95
- Finding
- Prompt Injection Through Unvalidated Brand and Domain Inputs<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, lines 95–111 **Vulnerability Type**: Prompt injection caused by direct interpolation of untrusted input into an LLM prompt **Risk Level**: Medium ### Vulnerable Code ```typescript // ONLY used when Methods 1 and 2 both fail // This is a MINIMAL prompt -- keep token usage as low as possible const response = await openai.chat.completions.create({ model: 'gpt-4.1-mini', max_tokens: 200, temperature: 0.3, messages: [ { role: 'system', content: 'You are a marketing analyst. Return only a JSON array of competitor objects.' }, { role: 'user', content: `List 5 direct competitors of "${brandName}"${domain ? ` (${domain})` : ''}. Return JSON: [{"name":"...","website":"...","reason":"..."}]` } ] }); ``` ### Technical Analysis The documented implementation directly interpolates the caller-controlled `brandName` and optional `domain` values into an OpenAI user message. No input validation, length restriction, character allowlist, escaping strategy, or structured separation between instructions and data is specified. An attacker can submit values containing additional natural-language instructions designed to override the intended request. Although the system message asks for a JSON array, it does not establish a strict machine-enforced response schema. The documented behavior merely instructs the implementation to parse the response, without requiring schema validation, domain validation, or rejection of unexpected fields and content. The vulnerable path is only reached when SerpAPI and DataForSEO fail or return insufficient results. Consequently, exploitation depends on triggering or encountering the fallback condition. This limits exploitability but does not remove the data-integrity risk. ### Attack Path 1. An attacker supplies a malicious `brandName` or `domain` containing embedded prompt instructions. 2. SerpAPI and DataForSEO fail, are unavailable, lack credentials, exhaust their q ...[truncated 1361 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Validate caller-controlled inputs before using them** - Apply conservative maximum lengths to `brandName` and `domain`. - Reject control characters, line breaks, markup, and other unnecessary characters. - Validate `domain` with a dedicated hostname parser and reject URLs, credentials, paths, queries, and non-hostname text. - Define an appropriate allowlist for brand-name characters while accounting for legitimate international names. 2. **Separate data from instructions** - Clearly identify input values as untrusted data. - Pass inputs in a structured representation rather than embedding them into free-form instructions. - Instruct the model explicitly not to interpret content inside data fields as commands. 3. **Enforce structured model output** - Use the API's schema-constrained structured-output capability rather than relying on a request to “return JSON.” - Define a schema that permits only an array of competitor objects with bounded `name`, `website`, and optional `reason` strings. - Reject additional properties and responses that do not conform exactly to the schema. 4. **Perform semantic output validation** - Normalize and validate every returned hostname. - Reject non-HTTP(S) schemes, IP literals where inappropriate, local or private-network destinations, credentials, paths used as hostnames, and malformed domains. - Enforce item-count and field-length limits. - Remove the input brand and domain using normalized, case-insensitive comparisons. - Treat all generated text as untrusted before rendering it into reports or passing it to other models. 5. **Fail safely** - If validation fails, return `EMPTY_COMPETITOR_DATA` with an appropriate error instead of attempting to repair or consume unsafe output. - Preserve the documented warning for fallback use and add telemetry for schema-validation failures, without logging secrets or excessive attacker-controlled content ...[truncated 316 chars]
