Replace eval() of model output in task handlers with literal parser (close #1567) - #1568
Open
Jiangrong-W wants to merge 1 commit into
Conversation
Three handlers passed the raw LLM reply through a cosmetic
JsonCleaner.extract_json_array_section() slice and then straight to eval():
- TaskOutputHandler.handle() (superagi/agent/output_handler.py)
- ReplaceTaskOutputHandler.handle() (superagi/agent/output_handler.py)
- QueueStepHandler._process_reply() (superagi/agent/queue_step_handler.py)
The cleaner only slices the substring between the first '[' and last ']'
without validating its contents, so a model reply such as
[__import__('os').system(...)] or [__import__('subprocess').getoutput(...)]
was evaluated as Python and executed arbitrary code on the backend host.
All three sinks ship on default-seeded paths: the
TaskOutputHandler/ReplaceTaskOutputHandler branches back the default
'Dynamic Task Queue-I' iteration workflow, and the TASK_QUEUE tool step that
reaches QueueStepHandler is seeded into the built-in Sales Engagement and
Recruitment workflows.
This is a single underlying vulnerability (unsafe eval of model output)
reachable through three distinct handler call-sites. The fix adds one shared
guard and wires it into all three:
- Add JsonCleaner.parse_array_section(), which extracts the array section
(reusing extract_json_array_section) and parses it with ast.literal_eval.
Legitimate replies (a literal list of task strings, in either single- or
double-quoted form as emitted by OpenAI) parse exactly as before, but
embedded function calls/imports now raise ValueError instead of executing.
Uses the stdlib ast module only; public signatures are unchanged.
- TaskOutputHandler.handle(), ReplaceTaskOutputHandler.handle() and
QueueStepHandler._process_reply() now call parse_array_section() instead
of extract_json_array_section()+eval().
Adds regression tests for each call-site asserting that os.system /
subprocess.getoutput payloads are rejected (ValueError) and never invoked,
and that a legitimate literal array is still parsed and enqueued.
Signed-off-by: christop <825583681@qq.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The task/step output handlers in the agent loop evaluated raw model output with
Python
eval(), which allowed a prompt-injected or adversarial model reply to runarbitrary code on the backend host. This change replaces the unsafe
eval()onmodel output with a single shared literal-only parser, so a legitimate array of
task strings is still accepted while any embedded code (function calls, imports)
is rejected. There is no user-facing behavior change for legitimate replies.
Related Issues
Closes #1567
Solution and Design
Root cause: three handlers took the raw LLM completion (
response['content']),passed it through the purely cosmetic
JsonCleaner.extract_json_array_section()(which only slices the substring between the first
[and the last], with nocontent validation), and then called
eval()on the result. Because the input isattacker-influenceable model text, a reply such as
[__import__('os').system(...)]or
[__import__('subprocess').getoutput(...)]was evaluated as Python and executedon the host.
The fix introduces one shared guard and wires it into all three call-sites:
JsonCleaner.parse_array_section()insuperagi/helper/json_cleaner.py.It reuses
extract_json_array_section()to isolate the array section, thenparses it with
ast.literal_eval(stdlib only — no new dependency). It returnsa
listfor a literal array and raisesValueErrorfor anything that is not aliteral list.
ast.literal_evalaccepts both single-quoted (str(dict)-style,as OpenAI emits) and double-quoted (JSON-style) arrays, so legitimate replies
parse exactly as before, but embedded function calls/imports can never execute.
Public signatures are unchanged.
TaskOutputHandler.handle(),ReplaceTaskOutputHandler.handle()andQueueStepHandler._process_reply()now callJsonCleaner.parse_array_section()instead of
extract_json_array_section()followed byeval().Why this closes the whole class: all three sinks shared the same root cause
(
eval()of model output after a cosmetic slice). Routing every call-site throughone literal-only parser removes
eval()from each path, so the same payload thatpreviously executed is now rejected with
ValueErrorat every site.Backwards compatibility: a legitimate literal list of task strings (single- or
double-quoted) parses to the same
listit did before, the downstreamnp.array(...).flatten().tolist()handling is unchanged, and all pre-existingunit tests continue to pass.
Affected sites (one underlying vulnerability, three call-sites — all fixed):
superagi/agent/output_handler.py:149—TaskOutputHandler.handle(eval(assistant_reply)on theoutput_type='tasks'branch of the default-seeded Dynamic Task Queue-I iteration workflow)superagi/agent/output_handler.py:180—ReplaceTaskOutputHandler.handle(eval(assistant_reply)on theoutput_type='replace_tasks'step of the same default workflow)superagi/agent/queue_step_handler.py:79—QueueStepHandler._process_reply(np.array(eval(assistant_reply))...on theTASK_QUEUEtool step seeded into the built-in Sales Engagement and Recruitment workflows)Test Plan
Added focused regression tests covering all three call-sites, plus a
backwards-compatibility assertion:
tests/unit_tests/agent/test_output_handler.py::test_task_output_handle_rejects_code_execution— an
os.systempayload wrapped in[...]raisesValueErrorand the patchedos.systemis never called.tests/unit_tests/agent/test_output_handler.py::test_replace_task_output_handle_rejects_code_execution— a
subprocess.getoutputpayload raisesValueErrorand the patchedsubprocess.getoutputis never called.tests/unit_tests/agent/test_queue_step_handler.py::test_process_reply_rejects_code_execution— an
os.systempayload raisesValueErrorand no task is enqueued.tests/unit_tests/agent/test_queue_step_handler.py::test_process_reply_parses_literal_array— a legitimate literal array is still parsed and the tasks are enqueued
(backwards compatibility).
Before the fix these tests fail (the payloads are executed / no
ValueErrorisraised); after the fix they pass. The full unit suite for the touched modules was
run locally and is green:
Type of change
Checklist