This PR replaces silent assert failures with structured DiffError raises in apply_patch, adds a missing path-traversal guard to the edit tool, and fixes a race condition and unhandled telemetry crash in event_service#3382
Conversation
| resolved_path = Path(file_path) | ||
| # Resolve path relative to workspace and guard against path traversal | ||
| resolved_path = (self.workspace_root / file_path).resolve() | ||
| if not str(resolved_path).startswith(str(self.workspace_root)): |
There was a problem hiding this comment.
While this makes perfect sense, I think, when we talked about this before, we settled on not constraining edit tools. The reason is that it may give a false sense of security: since the agent has terminal tool, it can do this anyway. The current SOTA LLMs don’t really blink for an error like this, they’ll figure out an alternative way.
I think of its environment as “agent’s box”, specially when run in docker/container/VM. Even locally, I know my laptop is “agent’s box” 😊 . Due to the power of its tools, it can do technically about anything in there
I do think the user may want better restrictions, however we have other means to enforce them: confirmations, security analyzers, sandboxing, hooks. WDYT?
|
Thank you for the contribution! Please let’s see what the agent thinks, too, because, you know… OpenHands is also one of our reviewers. 😅 @OpenHands /codereview this PR. Make sure you read inline threads too. Post your feedback as a review with gh api, with event. |
|
I'm on it! enyst can track my progress at all-hands.dev |
enyst
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of @enyst.
🔴 Taste Rating: Needs improvement
I read the PR diff and the existing inline thread. The direction is good — replacing optimized-away asserts with structured DiffErrors is exactly the right kind of hardening — but two of the claimed fixes are incomplete.
[CRITICAL ISSUES]
- [openhands-agent-server/openhands/agent_server/event_service.py, Line 486] Race condition still exists:
_main_loopis captured intomain_loop, but the scheduling call still dereferencesself._main_loop. A concurrentclose()can still setself._main_loop = Noneafter the check and before this line. Use the captured local (main_loop.run_in_executor(...)) or the race is not actually fixed. - [openhands-tools/openhands/tools/gemini/edit/impl.py, Line 47] Security/correctness: string-prefix containment is not a path boundary check. With a workspace like
/tmp/workspace,../workspace-escape/file.txtresolves outside the workspace but still passesstr(resolved).startswith(str(workspace_root)). Useresolved_path.is_relative_to(self.workspace_root)(or equivalent boundary-aware logic). Since this PR is hardening workspace containment, please also consider fixing the same pre-existing pattern inApplyPatchExecutor._resolve_pathrather than copying it.
[TESTING GAPS]
- [tests] These are behavior/security changes to tool execution and event emission, but the PR changes no tests. Please add focused regression tests for: malformed
apply_patchheaders returning structured errors; invalid*** Move to:paths; edit path traversal with a sibling-prefix case like../workspace-escape/file.txt; and the event-service_main_looprace fix. These should exercise real code paths, not mocks of the unit under test.
[POLICY NOTE]
- This PR changes tool execution behavior (
apply_patch, Gemini edit) and agent-server event emission. Per this repo's/codereviewguidance, I’m using aCOMMENTreview rather thanAPPROVEunless eval evidence is provided or a human maintainer decides the eval-risk policy is satisfied.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🔴 HIGH
This touches security-sensitive workspace path validation, tool execution semantics, and event-service concurrency. The intended safety improvements are valuable, but the current implementation leaves a real containment bypass and does not fully eliminate the race it claims to fix. Recommendation: Do not auto-merge; request human maintainer/security review after the boundary check and race fix are corrected and regression tests are added.
VERDICT:
❌ Needs rework: The core idea is worth merging, but the race fix and path-boundary check need to be made correct first.
KEY INSIGHT:
Hardening code must avoid approximate checks: local captures only help if you use them, and path containment needs filesystem path semantics, not string prefixes.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- When your PR is merged, the guideline file goes through normal code review by repository maintainers.
Resolve with AI? Install the iterate skill in your agent and run
/iterateto automatically drive this PR through CI, review, and QA until it's merge-ready.Was this review helpful? React with 👍 or 👎 to give feedback.
This comment was marked as duplicate.
This comment was marked as duplicate.
… path traversal - Replace all assert statements in apply_patch/core.py with explicit raise DiffError(...). Python asserts are disabled when running with -O (optimized mode), which Docker production images commonly use. A malformed patch would then silently corrupt state instead of returning a structured error to the agent. - The assert in process_patch() is particularly critical: AssertionError is not caught by the except DiffError block in definition.py, so a bad patch header crashes the tool entirely instead of returning is_error=True to the agent loop. - Validate move_to path in Parser.parse() to reject absolute paths and ".." components. The TODO comment in the original code acknowledged this check was missing. - Resolve workspace_root in EditExecutor.__init__ and check resolved file paths stay within the workspace boundary, matching the protection already present in ApplyPatchExecutor._resolve_path. Without this an LLM-supplied path like "../../etc/passwd" would read or overwrite files outside the sandbox workspace. - Fix race condition in _emit_event_from_thread: capture _main_loop and _conversation into local variables before the if-check so a concurrent close() call cannot set them to None between the check and the use. - Wrap log_callback body in try/except so a telemetry failure cannot propagate into the agent run and crash it.
- Remove extra blank line inside if-block in event_service.py - Wrap long DiffError f-string to fit line length in apply_patch/core.py - Remove unused 'import os' in gemini/edit/impl.py
45957aa to
670b4dc
Compare
Why
Six safety issues found across three files:
apply_patch/core.pyusesassertfor all internal validation. Python silently disablesassertwhen run with-O(optimized mode), which is common in Docker production images. A malformed patch silently corrupts state instead of returning an error.The
assertinsideprocess_patch()is especially critical —AssertionErroris not caught by theexcept DiffErrorblock indefinition.py, so a bad patch header crashes the entire tool instead of returningis_error=Trueto the agent.apply_patch/core.pyParser.parse()had a TODO comment acknowledging thatmove_topath validation was missing. An absolute or..-traversal move path was accepted without any check.gemini/edit/impl.pydid not resolveworkspace_rootbefore comparing paths. An LLM-supplied path like../../etc/passwdcould escape the sandbox.ApplyPatchExecutoralready had this protection via_resolve_path—EditExecutorwas missing the equivalent.event_service.py_emit_event_from_threadreadsself._main_loopandself._conversationtwice with no lock in between. A concurrentclose()call can set them toNonebetween the check and the use, causing anAttributeError.event_service.pylog_callbackhad no error handling. A telemetry failure could propagate and crash the entire agent run.Summary
assertstatements inapply_patch/core.pywith explicitraise DiffError(...)so validation always runs in production.move_topath validation inParser.parse()to reject absolute paths and..components.workspace_rootinEditExecutor.__init__and guard resolved paths against traversal outside the workspace boundary._main_loopand_conversationinto local variables before theif-check in_emit_event_from_threadto eliminate the race condition.log_callbackbody intry/exceptso telemetry failures are logged and swallowed instead of crashing the agent run.