Skip to content

fix(claws): harden ox install scripts against symlink escape and foreign checkouts#512

Merged
galexy merged 3 commits into
mainfrom
worktree-claws-install-hardening
Apr 14, 2026
Merged

fix(claws): harden ox install scripts against symlink escape and foreign checkouts#512
galexy merged 3 commits into
mainfrom
worktree-claws-install-hardening

Conversation

@galexy
Copy link
Copy Markdown
Contributor

@galexy galexy commented Apr 14, 2026

Summary

Round-2 follow-up to #511. CodeRabbit re-reviewed after the first round of fixes and flagged 3 criticals + 1 major that I acked before the merge — filing them now as a separate PR.

# Severity Where Problem
1 🔴 critical install-ox-git.sh mkdir -p ran BEFORE the symlink-escape check — a symlinked ancestor could let mkdir create dirs in a foreign tree before validation fired.
2 🔴 critical install-ox-git.sh An existing .git at the target path was accepted without verifying remote.origin.url, so a hand-edited invocation pointing at any user-owned repo would happily run its make install.
3 🟠 major update-ox.sh Text-level "$HOME"/* prefix check broke when $HOME was symlinked: install-ox-git.sh writes the PHYSICAL REAL_CLONE_PATH into state, so the stored path starts with $REAL_HOME and the textual $HOME check rejected every legit auto-update.
4 🔴 critical update-ox.sh git pull && make build && make install ran on the recorded clone_path without verifying it's actually sageox/ox. A hand-edited state file pointing at any user-owned repo under $HOME would execute that repo's Makefile on every invocation.

Fix shapes

#1 — walk up to the deepest existing ancestor, validate there, THEN mkdir.
Walking up from PARENT_DIR to the first directory that exists lets us resolve cd … && pwd -P on a real path, check it against $REAL_HOME, and verify ownership BEFORE any filesystem mutation. Once the ancestor is confirmed inside $HOME and user-owned, mkdir -p can only create plain directories underneath it — mkdir never creates symlinks, so the final physical parent is guaranteed to stay inside the validated ancestor.

#2 & #4 — `git -C remote get-url origin` check.
Both scripts now gate make build && make install on an origin-URL allow-list:

case "$(git -C "$real_clone_path" remote get-url origin 2>/dev/null || true)" in
  https://github.com/sageox/ox|https://github.com/sageox/ox.git) ;;
  git@github.com:sageox/ox|git@github.com:sageox/ox.git) ;;
  *) # reject / skip
esac

Covers HTTPS and SSH, with and without the .git suffix. Forks are intentionally rejected — users with forks can still use the curl install method or adjust their state file manually after understanding what they're opting into.

#3 — accept both `$HOME/` and `$REAL_HOME/` prefixes.
The text-level prefix check in update-ox.sh now accepts either form, so the resolved physical path stored by install-ox-git.sh passes through to the physical-resolution + ownership check below, which is where the real security decision happens. The text check stays as a cheap early filter for obviously-bad hand-edits.

Test plan

Smoke-tested each fix in isolation:

  • Fix 1 — created $HOME/escape-link → /tmp/ox-foreign, invoked install-ox-git.sh "$HOME/escape-link/deep/subdir" true. Before the fix, mkdir -p $HOME/escape-link/deep would create /tmp/ox-foreign/deep before validation. After the fix: script emits error: ancestor directory escapes $HOME via symlink: /private/tmp/ox-foreign and /tmp/ox-foreign/ stays empty.
  • Fix 2 — created a fake git repo at $HOME/fake-repo with origin=https://github.com/someone-else/not-ox.git, invoked install-ox-git.sh "$HOME/fake-repo" true. Script emits error: … is not a sageox/ox checkout (origin: https://github.com/someone-else/not-ox.git) and exits before touching Make.
  • Fix 3 — set HOME=/tmp/symlink-to-real-home where the symlink resolves to /tmp/ox-hardening-test, wrote state with clone_path=/private/tmp/ox-hardening-test/src/sageox-ox (the physical form), ran update-ox.sh. Script gets past the text prefix check and into the physical validation, which is what we want. (Before the fix, it would have emitted warning: clone_path not under $HOME and skipped every time.)
  • Fix 4 — set state clone_path to a git repo whose origin is https://github.com/someone-else/not-ox.git, ran update-ox.sh. Script emits warning: clone_path is not a sageox/ox checkout; skipping auto-update and falls through to the final readiness gate — make install never runs.
  • clawhub-skill-lint — PASS on both skills, 0 critical, 0 warnings.
  • Scripts byte-identical between sageox-distill and sageox-summary.
  • End-to-end first-run install against a throwaway ~/.openclaw/memory/ — deferred to reviewer.

Notes

Closes the round-2 findings on #511.

Summary by CodeRabbit

  • Improvements
    • Stronger filesystem-safety checks to avoid installing/updating into aliased, symlinked, or otherwise unsafe paths.
    • Installer/updater now refuse unknown or altered repositories and will skip or reject non-matching origins.
    • Updater refuses to proceed with uncommitted changes and now reliably aligns checkouts to the remote default before building.
    • Update mechanism hardened for more consistent build/install outcomes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 255c441f-d7d4-46b1-9c04-f628fbeecf49

📥 Commits

Reviewing files that changed from the base of the PR and between 017df1a and e693f90.

📒 Files selected for processing (4)
  • claws/openclaw/sageox-distill/scripts/install-ox-git.sh
  • claws/openclaw/sageox-distill/scripts/update-ox.sh
  • claws/openclaw/sageox-summary/scripts/install-ox-git.sh
  • claws/openclaw/sageox-summary/scripts/update-ox.sh

📝 Walkthrough

Walkthrough

Performs stricter filesystem and repository validations in four installer/updater shell scripts: resolves physical ancestors before mkdir, rejects symlinked clone targets, re-canonicalizes paths under REAL_HOME, and verifies the repo’s origin URL and a clean working tree before aligning and installing/updating from origin.

Changes

Cohort / File(s) Summary
Install scripts
claws/openclaw/sageox-distill/scripts/install-ox-git.sh, claws/openclaw/sageox-summary/scripts/install-ox-git.sh
Walks upward to the deepest existing ancestor and resolves it physically, verifies containment under resolved $HOME and ownership before mkdir -p. After creating parent, re-canonicalizes REAL_CLONE_PATH, rejects symlinked final components, ensures canonical path stays under REAL_HOME, requires git remote get-url origin to match allowed sageox/ox URL forms, and for existing checkouts enforces a clean working tree then git fetch, git remote set-head origin --auto and git reset --hard origin/HEAD before make build && make install.
Update scripts
claws/openclaw/sageox-distill/scripts/update-ox.sh, claws/openclaw/sageox-summary/scripts/update-ox.sh
Accepts both textual $HOME/* and resolved $REAL_HOME/* when checking clone_path. Adds origin allow-list verification via git -C ... remote get-url origin; refuses update if origin mismatches or working tree is dirty. Replaces git pull --ff-only with git fetch origin, git remote set-head origin --auto, and git reset --hard origin/HEAD, then runs make build && make install (failures logged and non-fatal).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as "User / Invoker"
participant Script as "install/update script\n(shell)"
participant FS as "Filesystem\n(parent dirs, symlinks)"
participant Git as "git / origin remote"
User->>Script: invoke with CLONE_PATH
Script->>FS: walk up dirname(CLONE_PATH) → deepest existing ancestor
FS-->>Script: ancestor path
Script->>FS: cd ancestor && pwd -P → REAL_ANCESTOR; check under REAL_HOME and ownership
alt ancestor validation fails
Script-->>User: exit (fail)
else ancestor ok
Script->>FS: mkdir -p parent (if needed); compute REAL_CLONE_PATH
Script->>FS: reject if final component is symlink; cd && pwd -P → canonical path
Script->>FS: verify canonical path remains under REAL_HOME
alt path escape or symlink detected
Script-->>User: exit (fail)
else path ok
Script->>Git: git -C REAL_CLONE_PATH remote get-url origin
Git-->>Script: origin URL
alt origin matches allowed patterns and working tree clean
Script->>Git: git fetch; git remote set-head origin --auto; git reset --hard origin/HEAD
Script->>Script: make build && make install
Script-->>User: success or logged non-fatal failure
else origin mismatch or dirty tree
Script-->>User: warn and skip/exit

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop through paths both near and far,

I sniff each link to find where you are,
No sly symlink may tread my ground,
I check the origin, safe and sound,
Then build and bloom where roots are found.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main security-focused changes: hardening ox install scripts against symlink escape vulnerabilities and foreign repository checkouts.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-claws-install-hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@claws/openclaw/sageox-distill/scripts/install-ox-git.sh`:
- Around line 164-168: REAL_CLONE_PATH must not be a symlinked final component;
after computing REAL_PARENT and REAL_CLONE_PATH, test the final basename with:
if [ -L "$REAL_PARENT/$(basename "$CLONE_PATH")" ]; then ...; and if true either
canonicalize by resolving the symlink with readlink -f and assign that to
REAL_CLONE_PATH before running ownership/origin checks, or fail fast with an
error asking the user to remove the symlink so the install can create/own the
directory; apply this change around the REAL_CLONE_PATH computation and before
the ownership check (references: REAL_CLONE_PATH, REAL_PARENT, CLONE_PATH,
update-ox.sh).
- Around line 181-189: The origin-checking case that evaluates ORIGIN_URL for
REAL_CLONE_PATH currently only accepts https:// and scp-style git@github.com:
remotes and rejects URI-style SSH remotes like
ssh://git@github.com/sageox/ox(.git); update the case patterns to also accept
the URI SSH form by adding a branch matching ssh://git@github.com/sageox/ox and
ssh://git@github.com/sageox/ox.git (i.e., include a pattern like
ssh://git@github.com/sageox/ox|ssh://git@github.com/sageox/ox.git alongside the
existing patterns) so the origin validation in the case statement allows that
format.

In `@claws/openclaw/sageox-distill/scripts/update-ox.sh`:
- Around line 131-138: The case statement that checks the output of git -C
"$real_clone_path" remote get-url origin currently only matches HTTPS and
scp-style SSH URLs; update the whitelist to also match URI-style SSH remotes by
adding patterns for ssh://git@github.com/sageox/ox and
ssh://git@github.com/sageox/ox.git (i.e., include the ssh://... and optional
.git variants) alongside the existing https://... and git@... patterns so valid
ssh:// URLs do not trigger the skip-update warning.

In `@claws/openclaw/sageox-summary/scripts/install-ox-git.sh`:
- Around line 164-168: REAL_CLONE_PATH may point to a symlinked final path
component so ownership/origin checks can be bypassed; after computing
REAL_CLONE_PATH from REAL_PARENT and basename "$CLONE_PATH" resolve that final
component to its canonical target (e.g., use readlink -f or realpath on
REAL_CLONE_PATH) and replace REAL_CLONE_PATH (and the value later saved as
clone_path) with the resolved path so the script operates on and records the
actual checkout location; ensure subsequent checks that reference
REAL_CLONE_PATH, REAL_PARENT and CLONE_PATH use the resolved value.
- Around line 181-189: The origin validation case for ORIGIN_URL currently only
matches HTTPS and scp-style SSH URIs but omits the ssh:// URI form; update the
case pattern list for ORIGIN_URL (the case statement that checks origin against
https://github.com/sageox/ox(.git) and git@github.com:sageox/ox(.git)) to also
accept ssh://git@github.com/sageox/ox and ssh://git@github.com/sageox/ox.git
(i.e., add patterns matching the ssh://git@github.com/sageox/ox with and without
.git) in the same case block, and apply the identical change in both scripts
that contain this ORIGIN_URL case check so those ssh:// URIs are treated as
valid origins.

In `@claws/openclaw/sageox-summary/scripts/update-ox.sh`:
- Around line 131-138: The case that validates the origin remote (the case on
the command that starts with case "$(git -C "$real_clone_path" remote get-url
origin 2>/dev/null || true)") ignores ssh:// URI-style remotes; update the
pattern list to also accept ssh://git@github.com/sageox/ox and
ssh://git@github.com/sageox/ox.git so the auto-update doesn't skip repositories
using the ssh:// URI form. Locate the case block that compares the remote URL
and add corresponding ssh://... entries alongside the existing https://... and
git@github.com:... patterns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f5fdb5f-1b84-48b3-9aa4-109f5d4aee64

📥 Commits

Reviewing files that changed from the base of the PR and between a4559d2 and aa9e440.

📒 Files selected for processing (4)
  • claws/openclaw/sageox-distill/scripts/install-ox-git.sh
  • claws/openclaw/sageox-distill/scripts/update-ox.sh
  • claws/openclaw/sageox-summary/scripts/install-ox-git.sh
  • claws/openclaw/sageox-summary/scripts/update-ox.sh

Comment thread claws/openclaw/sageox-distill/scripts/install-ox-git.sh
Comment thread claws/openclaw/sageox-distill/scripts/install-ox-git.sh
Comment thread claws/openclaw/sageox-distill/scripts/update-ox.sh
Comment thread claws/openclaw/sageox-summary/scripts/install-ox-git.sh
Comment thread claws/openclaw/sageox-summary/scripts/install-ox-git.sh
Comment thread claws/openclaw/sageox-summary/scripts/update-ox.sh
Comment thread claws/openclaw/sageox-distill/scripts/install-ox-git.sh
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@claws/openclaw/sageox-distill/scripts/install-ox-git.sh`:
- Around line 216-225: The origin URL check (ORIGIN_URL from git -C
"$REAL_CLONE_PATH" remote get-url) is spoofable because it trusts the existing
working tree; change the flow to fetch and reset to a trusted upstream ref
instead of executing code from the existing directory: after validating the
remote URL, perform a fresh fetch from that remote (or clone into a temporary
directory) and then reset the working tree to a known-good ref/commit/branch
(e.g., fetch origin and git reset --hard origin/<trusted-ref> or use a shallow
clone of sageox/ox into a temp path) before running any Makefile or other code;
ensure you reference REAL_CLONE_PATH/ORIGIN_URL in the checks and use the
freshly fetched/checked-out tree for subsequent execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6194e8f5-eecf-4126-a4c4-2899ce60b154

📥 Commits

Reviewing files that changed from the base of the PR and between aa9e440 and 017df1a.

📒 Files selected for processing (4)
  • claws/openclaw/sageox-distill/scripts/install-ox-git.sh
  • claws/openclaw/sageox-distill/scripts/update-ox.sh
  • claws/openclaw/sageox-summary/scripts/install-ox-git.sh
  • claws/openclaw/sageox-summary/scripts/update-ox.sh
✅ Files skipped from review due to trivial changes (1)
  • claws/openclaw/sageox-summary/scripts/update-ox.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • claws/openclaw/sageox-distill/scripts/update-ox.sh
  • claws/openclaw/sageox-summary/scripts/install-ox-git.sh

Comment thread claws/openclaw/sageox-distill/scripts/install-ox-git.sh
@galexy galexy merged commit 0f46346 into main Apr 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant