T09 · Insecure Skill Coding Practices
Error
- Location
- src/skill.ts:133
- Finding
- Caller-Controlled Simulator State Enables Prompt Injection and Safety Bypass<![CDATA[ ## Vulnerability Details **File Location**: `src/skill.ts:133-138`, `src/modes/conversationSim.ts:103-125`, `src/core/prompts.ts:170-187` **Vulnerability Type**: Unvalidated client-controlled state and indirect prompt injection **Risk Level**: High ### Vulnerable Code ```ts // src/skill.ts:133-138 const existingState = payload.state as SimState | undefined; const personaName = optionalString(payload.personaName); const state: SimState = existingState ?? createSimSession(personaName); const simInput: ConversationSimInput = { userMessage, state }; const result = await sendSimMessage(simInput); ``` ```ts // src/modes/conversationSim.ts:103-125 const safety = checkInput(userMessage); if (!safety.safe) { throw new SafetyError(safety.message ?? "Safety check failed."); } const stateWithUserTurn: SimState = { ...state, turns: [ ...state.turns, { speaker: "user", message: userMessage }, ], }; const prompt = buildSimPersonaPrompt( state.persona, stateWithUserTurn.turns, userMessage ); ``` ```ts // src/core/prompts.ts:170-187 const history = turns .map((t) => `${t.speaker === "user" ? "User" : persona.name}: ${t.message}`) .join("\n"); return `You are playing a character in a flirting conversation simulator. The user is practicing their texting game. Your character: Name: ${persona.name} Personality: ${persona.archetype} Description: ${persona.description} Warmth level: ${persona.warmthLevel}/3 (1 = reserved/guarded, 2 = moderate, 3 = warm/open) Conversation so far: ${history || "(No messages yet — this is the opening)"} User just sent: "${userMessage}" ``` ### Technical Analysis The routing layer treats `payload.state` as a valid `SimState` through a TypeScript assertion. Type assertions provide no runtime validation, so a caller can supply arbitrary values for: - `state.persona.name` - `state.persona.archetype` - `state.persona.description` - Historical turn messages and speaker values - `state.currentScore` - `state.m ...[truncated 2194 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Do not accept authoritative simulator state directly from callers. Store state server-side and let clients submit only an opaque, unpredictable session identifier. 2. If client-provided state is unavoidable, validate it with a strict runtime schema such as Zod, TypeBox, or JSON Schema. 3. Resolve the persona from the built-in `PERSONAS` collection using a trusted identifier. Do not accept caller-defined persona descriptions or archetypes. 4. Validate all state properties: - Permit only `user` and `persona` speaker values. - Require finite numeric scores within documented ranges. - Require a recognized momentum value. - Require a Boolean `sessionOver`. - Reject unknown properties where practical. 5. Run safety checks over all text entering the prompt, including every historical turn and all persona-related fields. 6. Limit message length, history length, and total serialized state size. Retain only the most recent turns needed by the simulator. 7. Delimit untrusted content clearly, preferably using structured content such as JSON, and explicitly instruct the model that quoted state is data and must never be followed as instructions. 8. Consider signing serialized state with an HMAC if it must round-trip through an untrusted client. Verify the signature before use. 9. Add adversarial tests covering instructions embedded in persona descriptions, persona names, and historical messages. ]]>
