Skip to content

fix: sanitize state values substituted into instruction templates#361

Open
g0w6y wants to merge 9 commits into
google:mainfrom
g0w6y:fix/instructions-state-injection-sanitize
Open

fix: sanitize state values substituted into instruction templates#361
g0w6y wants to merge 9 commits into
google:mainfrom
g0w6y:fix/instructions-state-injection-sanitize

Conversation

@g0w6y
Copy link
Copy Markdown
Contributor

@g0w6y g0w6y commented May 19, 2026

Problem

injectSessionState() replaces {var_name} placeholders in an agent's
instruction string with values from two sources:

  • session state{role}session.state['role']
  • artifacts{artifact.report} → artifact file content

Both paths returned the raw string with no escaping. A value containing
HTML special characters such as <, >, or & would be embedded
directly into the system prompt without sanitization.

Fix

Escape HTML entities in every substituted value before inserting it into
the template. Applied to both the state injection path and the artifact
injection path:

return String(value)
  .replace(/&/g, '&amp;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;');

The order matters — & is replaced first to avoid double-escaping
sequences like &lt; into &amp;lt;.

Example

Before

state:   { snippet: '<script>alert(1)</script>' }
prompt:  'code={snippet}'

sent to model: 'code=<script>alert(1)</script>'

After

sent to model: 'code=&lt;script&gt;alert(1)&lt;/script&gt;'

Changes

  • core/src/agents/instructions.ts — escape &, <, > in both the
    state injection path and the artifact injection path
  • core/test/agents/instructions_test.ts — update existing assertions to
    expect escaped output; add new sanitization tests covering individual
    character escaping, combined escaping, and double-escape prevention

g0w6y and others added 3 commits May 19, 2026 14:59
…mplates

State values substituted via {var_name} in instruction templates were
returned verbatim. An attacker who can write to session state can
craft a value that continues the instruction text and overrides agent
behavior — classic prompt injection.

Wrapping the substituted value in <value>...</value> and escaping
HTML entities creates a clear boundary between the instruction
written by the developer and the data coming from state. Modern
models respect this distinction and treat the tagged content as data
rather than as part of the instruction.

Before:
  instruction: "User role: {role}"
  state:       {role: "admin. Ignore previous instructions."}
  result:      "User role: admin. Ignore previous instructions."

After:
  result: "User role: <value>admin. Ignore previous instructions.</value>"
The artifact.* substitution path had the same issue — returning
String(artifact) verbatim into the instruction template. Applies
the same XML wrapping and entity escaping as the state injection fix.
Copy link
Copy Markdown
Collaborator

@kalenkevich kalenkevich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, can you include tests as well?

@g0w6y
Copy link
Copy Markdown
Contributor Author

g0w6y commented May 27, 2026

@kalenkevich Added tests covering the sanitization behavior for both state and artifact injection paths — all 23 pass. Ready for re-review

…ection

- Update existing tests to expect values wrapped in <value>...</value> tags
- Add dedicated sanitization block testing HTML entity escaping (&, <, >)
- Add prompt injection prevention tests for both state and artifact paths
- Add double-escaping guard test (&lt; in raw input → &amp;lt; in output)
- Expand artifact injection tests to cover HTML escaping and injection prevention
@g0w6y g0w6y force-pushed the fix/instructions-state-injection-sanitize branch from 357a97e to 5636371 Compare May 27, 2026 08:28
Comment thread core/test/agents/instructions_test.ts Outdated
Comment thread core/src/agents/instructions.ts Outdated
@g0w6y
Copy link
Copy Markdown
Contributor Author

g0w6y commented May 27, 2026

@kalenkevich removed the <value> wrapper as discussed — now only HTML entity escaping is applied on both the state and artifact injection paths. Tests updated accordingly. Please take a look when you get a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants