Skip to content

Fix path traversal in imported hook-transform realignment - #117

Open
jay79-boop wants to merge 1 commit into
chrysb:mainfrom
jay79-boop:fix/import-hook-transform-path-traversal
Open

Fix path traversal in imported hook-transform realignment#117
jay79-boop wants to merge 1 commit into
chrysb:mainfrom
jay79-boop:fix/import-hook-transform-path-traversal

Conversation

@jay79-boop

Copy link
Copy Markdown

Summary

alignHookTransforms() (lib/server/onboarding/import/import-applier.js) derives filesystem paths from an imported repo's own openclaw.json contentmatch.path (hookPath) and transform.module (actualModule) — via normalizeHookPath() / normalizeTransformModulePath(), which only strip a leading slash:

const normalizeHookPath = (value) => String(value || "").trim().replace(/^\/+/, "");
const normalizeTransformModulePath = (value) =>
  String(value || "").trim().replace(/^\/+/, "").replace(/^hooks\/transforms\/+/, "");

Neither rejects ../. Those values flow straight into path.join(baseDir, "hooks/transforms", ...) with no containment check, for both:

  • a read/move step — whatever exists at the "actual" module path gets moved into a _backup directory
  • an unconditional fs.writeFileSync() of a generated shim at the "expected" module path

This runs during onboarding's "import an existing OpenClaw workspace" flow — a first-class, encouraged feature (e.g. restoring from a previous deployment's git-synced config repo), not an edge case. A crafted openclaw.json in the imported repo can:

  • point transform.module far enough up via ../ to reach an existing host file outside the temp clone directory, which then gets moved into the imported workspace's _backup dir — arbitrary file read/exfiltration, since it becomes browsable (and later git-synced) as part of the new AlphaClaw config
  • point match.path via ../ so the shim's write path (baseDir/hooks/transforms/<hookPath>/<hookPath>-transform.mjs) lands outside the temp dir entirely — arbitrary file write, unconditional, no existence check gates the write side

Fix

Both derived-path computations (actualAbsolutePath/expectedAbsolutePath, and the backup-root pair built from them) are now checked against baseDir before anything reads, moves, or writes — mirroring the containment check already used correctly elsewhere in this same file (resolveExtractionTargetPath, used by applySecretExtraction and canonicalizeConfigEnvRefs). A mapping whose paths escape baseDir is now just skipped instead of processed.

Test plan

  • Added two tests to tests/server/import-applier.test.js, verified to fail without the fix (alignedCount: 1, and the read/write actually happening) and pass with it:
    • a transform.module escape reaching a real external "secret" file: proves the file is never moved/read and nothing changes outside baseDir
    • a match.path escape targeting an external write location: proves no shim is written outside baseDir and the legitimate in-workspace transform is left untouched
  • Full import-applier.test.js suite passes (6/6), including the existing legitimate-realignment coverage.

alignHookTransforms() derives filesystem paths from an imported
repo's own openclaw.json content -- match.path (hookPath) and
transform.module (actualModule) -- via normalizeHookPath() /
normalizeTransformModulePath(), which only strip a leading slash.
Neither rejects `../`. Those values then flow straight into
path.join(baseDir, "hooks/transforms", ...) with no containment
check, for both a read/move step (moving whatever exists at the
"actual" module path into a _backup dir) and an unconditional
fs.writeFileSync() of a generated shim at the "expected" module path.

Since this runs during onboarding's "import an existing OpenClaw
workspace" flow -- a first-class feature, not an edge case -- a
crafted openclaw.json in the imported repo could:
  - point transform.module far enough up via ../ to reach an
    existing host file outside the temp clone directory, which then
    gets *moved* into the imported workspace's _backup dir (arbitrary
    file read/exfiltration: it becomes browsable, and later
    git-synced, as part of the new AlphaClaw config)
  - point match.path via ../ so the shim's write path
    (baseDir/hooks/transforms/<hookPath>/<hookPath>-transform.mjs)
    lands outside the temp dir entirely (arbitrary file write,
    unconditional -- no existence check gates the write side)

Both derived-path computations (actualAbsolutePath/expectedAbsolutePath,
and the backup-root pair built from them) now get checked against
baseDir before anything reads, moves, or writes -- mirroring the
containment check already used correctly elsewhere in this same file
(resolveExtractionTargetPath, used by applySecretExtraction and
canonicalizeConfigEnvRefs). A mapping whose paths escape baseDir is
now just skipped instead of processed.

Added two tests to tests/server/import-applier.test.js, verified to
fail without the fix (alignedCount: 1, and the read/write actually
happening) and pass with it:
  - a transform.module escape reaching a real external "secret" file:
    proves the file is never moved/read and nothing changes outside
    baseDir
  - a match.path escape targeting an external write location: proves
    no shim is written outside baseDir and the legitimate in-workspace
    transform is left untouched
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