Skip to content

[Security] nano-banana-pro follows output symlinks and overwrites files outside the working directory #46

Description

@DillyRabbit

Hi, I found a symlink-following issue in the nano-banana-pro skill that can overwrite files outside the working directory.
GitHub: https://github.com/steipete/agent-scripts/tree/main/skills/nano-banana-pro
ClawHub: https://clawhub.ai/steipete/skills/nano-banana-pro

Summary

generate_image.py saves the generated image to the path supplied with --filename without rejecting symlinks or checking that the resolved path stays inside the working directory.

If an untrusted repository contains result.png as a symlink to a file outside the repository, running the skill from that repository with --filename result.png follows the link and overwrites the target with PNG data. This can corrupt or destroy files that the OpenClaw process can write.

Steps to reproduce

  1. Create an isolated test directory:

    mkdir -p /tmp/nbp-test/workspace /tmp/nbp-test/outside
    printf 'SAFE TEST SENTINEL\n' > /tmp/nbp-test/outside/victim.txt
    ln -s ../outside/victim.txt /tmp/nbp-test/workspace/result.png
  2. From the workspace, run the skill with a valid Gemini API key available through GEMINI_API_KEY:

    cd /tmp/nbp-test/workspace
    python3 /path/to/nano-banana-pro/scripts/generate_image.py \
      --prompt "A simple blue square on a plain light background." \
      --filename result.png
  3. Check the target:

    file /tmp/nbp-test/outside/victim.txt

Expected behavior

The script should reject symlink outputs and any resolved output path outside the working directory.

Actual behavior

The script follows result.png and writes the generated PNG to /tmp/nbp-test/outside/victim.txt.

Impact

An attacker who can provide or modify repository contents can place the output symlink in the repository. If a user or agent runs image generation from that repository using the symlink name, files outside the repository may be overwritten or corrupted, subject to the OpenClaw process's file permissions.

Affected versions

Confirmed in nano-banana-pro 1.0.0 and 1.0.1.

Suggested fix

Reject symlink outputs and verify the resolved destination remains within the intended output directory before writing. Consider requiring an explicit overwrite option when the destination already exists.

Activity

  1. added
    P2Normal priority bug or improvement with limited blast radius.
    clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.
    clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.
    clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.
    impact:data-lossThis issue is about lost, corrupted, or silently dropped user/session/config data.
    impact:securityThis issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.
    issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.
    on Oct 7, 2026
  2. clawsweeper commented on Oct 7, 2026

    @clawsweeper

    Codex review: this still needs some work. Reviewed October 6, 2026, 11:19 PM ET / October 7, 2026, 03:19 UTC.

    Summary
    The reported overwrite mechanism remains reachable on current main and in release 0.12.0. This is an owned security defect; no open fixing PR was found.

    Reproducibility: yes. from source: a relative output symlink reaches Pillow's truncating filename writer when Gemini returns an image. No filesystem-writing reproduction or live API call was executed during this read-only review.

    Next step
    Security-sensitive filesystem hardening needs owner-led repair and adversarial validation before automated implementation intake.

    Security
    Needs attention: Current source confirms an attacker-controlled output symlink can redirect a destructive image write.

    Review details

    Best possible solution:

    Use a race-resistant output writer that cannot follow attacker-controlled links, preserves explicitly selected output directories, and verifies that rejected destinations leave outside files untouched.

    Do we have a high-confidence way to reproduce the issue?

    Yes, from source: a relative output symlink reaches Pillow's truncating filename writer when Gemini returns an image. No filesystem-writing reproduction or live API call was executed during this read-only review.

    Is this the best way to solve the issue?

    Partly: rejecting redirected outputs addresses the defect, but standalone path checks are race-prone. Blanket working-directory confinement also conflicts with documented explicit output paths; safe file creation should enforce the intended destination at the write boundary.

    AGENTS.md: found and applied where relevant.

    Remaining risk / open question:

    • A pre-save symlink or resolve check alone would remain vulnerable to path replacement races and symlinked parent directories.

    Codex review notes: model internal, reasoning medium; reviewed against 79150cfac4a6.

    Label changes

    Label changes:

    • add P2: This is a concrete security defect in an optional image-generation skill, requiring an attacker-controlled destination and a successful image-generation run.
    • add impact:data-loss: The filename-based writer truncates writable symlink targets and replaces their contents with image data.
    • add impact:security: Repository-controlled symlinks can redirect a write to a destination outside the user's intended output location.
    • add issue-rating: 🦞 diamond lobster: Current issue advisory state selects this label.
    • add clawsweeper:source-repro: Current issue advisory state selects this label.
    • add clawsweeper:no-new-fix-pr: Current issue advisory state selects this label.
    • add clawsweeper:needs-security-review: Current issue advisory state selects this label.

    Label justifications:

    • P2: This is a concrete security defect in an optional image-generation skill, requiring an attacker-controlled destination and a successful image-generation run.
    • impact:security: Repository-controlled symlinks can redirect a write to a destination outside the user's intended output location.
    • impact:data-loss: The filename-based writer truncates writable symlink targets and replaces their contents with image data.
    Evidence reviewed

    Security concerns:

    • [high] Output symlinks redirect truncating writes — skills/nano-banana-pro/scripts/generate_image.py:163
      The image-save branches pass an unchecked filename to Pillow, which opens it for truncating writes. An attacker-controlled symlink can consequently overwrite any target writable by the invoking process.
      Confidence: 0.99

    What I checked:

    • Current output path lacks protection: The script constructs the requested path and creates its parent directories without rejecting symlinks. All three image-mode branches pass that path directly to Pillow; resolve() runs only after saving. (skills/nano-banana-pro/scripts/generate_image.py:92, 79150cfac4a6)
    • Dependency confirms destructive filename semantics: The target imports PIL.Image and calls its save method with a filename, establishing an affirmative dependency contract. Pillow's Image.save opens filename outputs with builtins.open(filename, "w+b"), which follows filesystem symlinks and truncates existing targets. It also supports caller-opened binary file objects, allowing the target to control safe file creation. (src/PIL/Image.py:2706, 68a3fe957d8a)
    • Existing output-directory contract: The skill instructs agents to run from the user's working directory, recommends a new filename each run, and explicitly permits a specified output path containing a directory. Mandatory working-directory confinement would therefore change an existing documented capability. (skills/nano-banana-pro/SKILL.md:118, 79150cfac4a6)
    • Latest release retains the unsafe writer: The 0.12.0 tag contains the same three filename-based save calls and post-save resolve operation. The supplied latest-release identity matches tag commit b7f7269; no fixed release is established. (skills/nano-banana-pro/scripts/generate_image.py:161, b7f726924927)
    • Related-work check: GitHub REST listings returned this issue as the only open item and no open PR. The supplied merged reference, docs(codex-first): cover ChatGPT-app-bundled Codex on PATH #29, concerns launching an app-bundled Codex binary through symlinks, not image-output writes, and does not fix this report.
    • Area history and inspection limitation: Available local history and GitHub commit metadata connect steipete to the skill's model update and earlier versioning work. The inspected model-update patch leaves the output writer unchanged. Local blame and deeper history searches failed because a required historical object could not be fetched (HTTP 403); vulnerability introduction was not established. (skills/nano-banana-pro/scripts/generate_image.py:130, 27b549436127)

    Likely related people:

    • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
    How this review workflow works
    • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
    • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
    • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
    • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
    • Maintainers can also comment @clawsweeper review to request a fresh review only.
    • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
    • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
    • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
  3. steipete commented on Oct 10, 2026

    @steipete
    Owner

    Fixed in #47, merged as d9f70f4. Thanks for the clear report.

    Reproduced with real Pillow writes and synthetic Gemini responses: the original script followed the output symlink into an outside sentinel file. The writer now creates outputs exclusively through pinned, no-follow directory handles, so existing files, hard links, dangling symlinks, and symlinked parent paths are rejected. Explicit output directories remain supported.

    All ten offline regression tests pass, including leaf/parent replacement races and image conversion:
    python3 -m unittest discover -s skills/nano-banana-pro/scripts -p 'test_*.py'

    Independent review found no actionable P0–P2 findings. Both PR CI and post-merge main CI passed. This fix is on main; no release was published in this round.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:data-lossThis issue is about lost, corrupted, or silently dropped user/session/config data.impact:securityThis issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions