Replace eval() with AST-based coercion in CodeAct return_result - #31
Closed
alessiodevoto wants to merge 1 commit into
Closed
Replace eval() with AST-based coercion in CodeAct return_result#31alessiodevoto wants to merge 1 commit into
alessiodevoto wants to merge 1 commit into
Conversation
…sult _maybe_eval_constructor_string previously eval'd LLM-supplied constructor strings with session_locals as the eval namespace, letting a prior cell's objects (e.g. planted callables or attribute chains) execute via a return_result argument. Replace eval() with ast.parse(mode=\"eval\"), require the call target to match the declared return type, restrict args to Python literals and plain-JSON session values (copied via json.loads(json.dumps(...))), and reject nested calls, attribute access, and **kwargs unpacking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Collaborator
Author
|
Closing without merging.
|
alessiodevoto
deleted the
security/codeact-constructor-eval-session-locals
branch
August 3, 2026 06:55
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.
Summary
Fixes an indirect code-execution path in CodeAct's
return_resultargument coercion.The issue
When the model calls
return_resultas a tool with a stringified constructor (e.g.{"result": "Answer(answer=1, reason='ok')"}),_maybe_eval_constructor_stringinsrc/nooa/strategies/codeact.pycoerces that string into an actualAnswerinstance. The previous implementation did this witheval(stripped, {"__builtins__": {}}, session.session_locals).session.session_localsaccumulates every object created by priorexecute_pythoncells (populated fromresult.captured_localsatcodeact.py:1506and2446). Stripping__builtins__blockedopen/__import__etc., but it did not block attribute access or method invocation on session objects. A constructor string likeAnswer(answer=planted.trigger())— whereplantedwas any object bound in an earlier cell — would executeplanted.trigger()as a side effect of coercion.Impact: the
return_resulttool-call surface is nominally a "return a value" boundary, but this made it a second path to arbitrary code execution driven by LLM-controlled strings, distinct from the intendedexecute_pythoncell path. Concretely relevant when the payload flows in from a less-trusted layer (e.g. a delegated/subagent result) that a caller might assume is inert.Fix
Replace
eval()with an AST-based coercion:ast.parse(mode="eval")and require a bareCall(func=Name(...)).call.func.id == return_type.__name__andisinstance(base_return_type, type)— no fallback to arbitrary session-local classes.Annotated[T, ...]return types are unwrapped via_extract_annotated_description.**kwargsunpacking (kw.arg is None); reject nested calls, attribute access, subscripting, and starred args (they fall through toast.literal_evalwhich raises).Namereferences to plain-JSON session values only (exactbool/int/float/str/list/tuple/dict, no subclasses). Session values are detached viajson.loads(json.dumps(..., allow_nan=False))before use, so custom__getitem__/ dunder methods on subclassed containers can't leak intoreturn_type(**...).base_return_type(*args, **kwargs)directly rather than eval'ing source.Behavior changes
_maybe_eval_constructor_stringis a fallback for a documented anti-pattern (LLMs callingreturn_resultas a tool with a stringified constructor instead ofreturn_result(value)from insideexecute_python; see the guidance atcodeact.py:564). The sanctioned paths are unaffected:return_result(literal)— works.return_result(var)wherevaris any session local — works (handled by the variable-ref resolution path atcodeact.py:1792, which runs before coercion).return_result("ClassName(field=literal, ...)")with only literals or plain-JSON session values — works, values are JSON-copied.Newly rejected (each falls back to Pydantic validation, which fails and the LLM retries):
Answer(answer=min(3, 1, 2)).Answer(reason=obj.description).return_result(var)still works — only the "re-wrap in a constructor call" form is blocked.Test plan
tests/strategies/test_codeact_pure_python_coverage.py::TestMaybeEvalConstructorString— 12/12 pass, including new regression tests:test_constructor_does_not_call_session_object— planted callable in session_locals is not invoked.test_constructor_rejects_non_plain_session_object— arbitrary object references rejected without triggering__iter__/__repr__.test_constructor_with_session_value_is_json_detached— plain values pass through and are copied (identity-distinct from the source).test_nested_calls_return_as_is— replaces the oldtest_nested_parens_work; nested calls are now rejected.