Skip to content

security: strip credentials from migration snapshots and enforce blueprint digest#156

Open
gn00295120 wants to merge 6 commits intoNVIDIA:mainfrom
gn00295120:security/sandbox-credential-exposure-and-blueprint-bypass
Open

security: strip credentials from migration snapshots and enforce blueprint digest#156
gn00295120 wants to merge 6 commits intoNVIDIA:mainfrom
gn00295120:security/sandbox-credential-exposure-and-blueprint-bypass

Conversation

@gn00295120
Copy link
Contributor

@gn00295120 gn00295120 commented Mar 17, 2026

Summary

Fixes two critical vulnerabilities that together enable a one-message supply chain attack: a single Telegram message can steal all host credentials from the sandbox and use them to push backdoors to GitHub and npm.

Vulnerability 1 (CRITICAL): Migration copies all host credentials into sandbox

createSnapshotBundle() in migration-state.ts calls cpSync(hostState.stateDir, ...) which copies the entire ~/.openclaw directory — including auth-profiles.json with live API keys, GitHub PATs, and npm tokens — verbatim into the sandbox filesystem.

A compromised agent (e.g., via the Telegram bridge injection in #118) can read /sandbox/.openclaw/agents/main/agent/auth-profiles.json and exfiltrate all credentials via api.telegram.org (which is allowed in the baseline network policy).

Fix: Two sanitization layers:

  • prepareSandboxState()sanitizeCredentialsInBundle(): deletes auth-profiles.json from agents/ subtree, strips credential fields from openclaw.json
  • createSnapshotBundle()sanitizeExternalRootSnapshot(): strips auth-profiles.json from external root snapshots (agentDir, workspace, skills) before archiving

Vulnerability 2 (HIGH): Blueprint integrity verification silently bypassed

verifyBlueprintDigest() in verify.ts checks if (manifest.digest && ...) — but blueprint.yaml ships with digest: "" which is falsy in JavaScript. The entire integrity check is silently skipped.

Fix: Empty/missing digest is now a hard verification failure. Also fixed parseManifestHeader in resolve.ts to properly strip YAML quotes and inline comments from the digest field via cleanYamlValue().

Changes by file

File Change
migration-state.ts Add stripCredentials(), sanitizeCredentialsInBundle(), sanitizeExternalRootSnapshot(), removeAuthProfileFiles()
verify.ts Empty digest → hard failure instead of silent pass
resolve.ts Add cleanYamlValue() — strips YAML quotes and inline comments from manifest fields
test/security-credential-exposure.test.js 9 tests: credential exposure PoC, digest bypass verification, fix verification

Attack chain (now broken at two points)

Telegram message → shell injection → read auth-profiles.json
                                     ↑ BLOCKED: file deleted
                                     
Blueprint tampering → digest: "" → verification passes
                                   ↑ BLOCKED: hard failure

Related PRs

PR Relationship
#148 Fixes shell injection root cause (run()runArgv())
#119 Fixes Telegram bridge injection (different file, no conflict)
#118 Issue: Telegram bridge injection — not addressed by this PR
#123 Fixes dangerouslyDisableDeviceAuth (different file, no conflict)

Test plan

  • 60 tests pass (51 original + 9 new security tests)
  • Manual: openclaw nemoclaw migrate completes with sanitized bundle
  • Manual: Verify sandbox agent functions without baked-in credentials (uses OpenShell provider injection)

Note on breaking change

The shipped blueprint.yaml has digest: "". After this change, workflows using that blueprint will fail verification until the digest is populated at release time. This is intentional — running with an unverified blueprint should be an explicit operator decision, not a silent default.

Summary by CodeRabbit

  • Security

    • Migration snapshots and bundles now remove authentication files and redact credential values with a visible sentinel to prevent accidental exposure during state preparation and packaging.
  • Tests

    • Added end-to-end tests validating auth file removal, credential redaction, preservation of non-credential configuration, and workspace file integrity.
  • Chores

    • Commit rules updated to allow a new "security" commit type.

Copilot AI review requested due to automatic review settings March 17, 2026 08:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens sandbox migration and blueprint integrity checks to prevent credential exposure in the sandbox bundle and to ensure blueprint tampering cannot bypass verification via an empty digest.

Changes:

  • Sanitize migration sandbox bundle output by removing auth-profiles.json and stripping credential-like fields from openclaw.json.
  • Update blueprint digest verification to fail when the manifest digest is missing/empty.
  • Add a new security-focused test/PoC file intended to demonstrate the vulnerabilities and verify the fixes.

Reviewed changes

Copilot reviewed 3 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/security-credential-exposure.test.js Adds a security PoC/test suite for credential exposure + digest verification behavior.
nemoclaw/src/commands/migration-state.ts Adds credential sanitization helpers and applies them during sandbox bundle preparation.
nemoclaw/src/blueprint/verify.ts Treats empty/missing digest as a verification error instead of silently passing.
nemoclaw/dist/commands/migration-state.js Built output reflecting the new sanitization behavior.
nemoclaw/dist/commands/migration-state.js.map Source map update for the built migration-state output.
nemoclaw/dist/commands/migration-state.d.ts.map Type map update for the built migration-state output.
nemoclaw/dist/blueprint/verify.js Built output reflecting the new empty-digest verification failure.
nemoclaw/dist/blueprint/verify.js.map Source map update for the built verify output.
nemoclaw/dist/blueprint/verify.d.ts.map Type map update for the built verify output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch 3 times, most recently from edb87b5 to a5b3097 Compare March 17, 2026 09:25
@gn00295120 gn00295120 requested a review from Copilot March 18, 2026 13:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens NemoClaw’s migration and blueprint handling by preventing credential leakage into the sandbox and tightening blueprint integrity verification.

Changes:

  • Sanitize migration snapshots/bundles by removing auth-profiles.json and stripping credential-like fields from openclaw.json.
  • Treat missing/empty blueprint digests as a verification failure and add basic YAML value “cleaning” for manifest header parsing.
  • Add a security-focused test suite intended to cover credential exposure and digest enforcement scenarios.

Reviewed changes

Copilot reviewed 4 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/security-credential-exposure.test.js Adds security tests for credential exposure and blueprint digest enforcement (but currently reimplements logic instead of calling production code).
nemoclaw/src/commands/migration-state.ts Adds credential sanitization for prepared sandbox state and external root snapshots.
nemoclaw/src/blueprint/verify.ts Fails verification when manifest digest is missing/empty; improves error messaging.
nemoclaw/src/blueprint/resolve.ts Adds cleanYamlValue to strip quotes/comments from manifest header fields (currently has a parsing bug for quoted values with comments).
nemoclaw/dist/commands/migration-state.js Built output for migration sanitization changes.
nemoclaw/dist/commands/migration-state.js.map Sourcemap update for migration-state build output.
nemoclaw/dist/commands/migration-state.d.ts.map Type sourcemap update for migration-state build output.
nemoclaw/dist/blueprint/verify.js Built output for blueprint digest verification changes.
nemoclaw/dist/blueprint/verify.js.map Sourcemap update for blueprint verify build output.
nemoclaw/dist/blueprint/verify.d.ts.map Type sourcemap update for blueprint verify build output.
nemoclaw/dist/blueprint/resolve.js Built output for blueprint manifest parsing changes.
nemoclaw/dist/blueprint/resolve.js.map Sourcemap update for blueprint resolve build output.
nemoclaw/dist/blueprint/resolve.d.ts.map Type sourcemap update for blueprint resolve build output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 19, 2026
1. cleanYamlValue: remove inline comment BEFORE stripping surrounding
   quotes so that `""  # Computed at release time` correctly yields an
   empty string instead of the still-quoted `""`.

2. cleanYamlValue: broaden the inline-comment regex from `/\s+#\s/` to
   `/\s+#/` so that YAML comments without a trailing space (e.g.
   `abc123 #no-space`) are also stripped, matching the YAML spec.

3. walkAndRemoveFile: wrap `readdirSync(dirPath)` in its own try/catch
   and return early on failure so permission errors or transiently
   missing directories cannot abort snapshot creation (the function is
   documented as non-fatal).

4. Manifest-parsing tests now call the production `readCachedManifest`
   / `getCacheDir` exports instead of reimplementing the parsing logic,
   ensuring regressions in `cleanYamlValue` or `parseManifestHeader`
   are caught by the test suite.

5. "Fix verification" tests now call `detectHostOpenClaw` +
   `createSnapshotBundle` from the production migration-state module and
   assert on the produced bundle, replacing the previous duplicate local
   `stripCredentials` implementation.  A second negative test confirms
   that raw credential strings are absent from the bundle.

All 9 tests pass after the changes (node --test).
@wscurran wscurran added Migration Use this label to identify issues with migrating to NemoClaw from another framework. security Something isn't secure labels Mar 20, 2026
@wscurran
Copy link
Contributor

Thanks for addressing the critical vulnerabilities related to migration snapshots and blueprint digests, this could significantly improve the security of NemoClaw.

@wscurran wscurran added priority: high Important issue that should be resolved in the next release Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. labels Mar 20, 2026
@cv
Copy link
Contributor

cv commented Mar 21, 2026

Really appreciate the security work here, @gn00295120 — stripping credentials from migration snapshots and enforcing blueprint digest are both important hardening steps. Since this was opened, we've added CI checks and shipped quite a few changes, so the base has drifted a fair bit. Could you rebase this onto the latest main so we can evaluate it against the current state of things? Would love to get a fresh look at this. Thanks for the effort!

@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch from caa669d to 77fbc62 Compare March 21, 2026 23:10
@gn00295120
Copy link
Contributor Author

Thanks @cv — rebased onto latest main. Here's what changed during the rebase:

Scope reduced to credential sanitization only. The blueprint digest bypass fix (verify.ts / resolve.ts) was dropped because those modules were deleted upstream in #492 when the openclaw nemoclaw CLI commands were removed. The vulnerability no longer exists since there's no code path that calls the verification logic.

What remains (2 files changed):

  • migration-state.ts: sanitizeCredentialsInBundle() + sanitizeExternalRootSnapshot() — strips auth-profiles.json and credential fields from openclaw.json before the bundle enters the sandbox
  • test/security-credential-exposure.test.js: 2 tests (credential exposure PoC + fix verification), down from 9 — blueprint-related tests removed

The credential sanitization merged cleanly with the current codebase, including the pyright strict typing changes from #523. All tests pass.

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds credential-sanitization utilities and applies them during migration bundle/snapshot creation: defines credential field allowlist, recursively strips credential values from openclaw.json, removes agents/*/auth-profiles.json, and sanitizes copied external-root snapshots before archiving.

Changes

Cohort / File(s) Summary
Credential sanitization logic
nemoclaw/src/commands/migration-state.ts
Adds CREDENTIAL_FIELDS allowlist and stripCredentials(); calls sanitizers to remove agents/*/auth-profiles.json, strip credential fields from written openclaw.json, and sanitize external-root snapshots after copying.
Security tests
test/security-credential-exposure.test.js
Adds tests reproducing pre-fix behavior and verifying post-fix: deletion of auth-profiles.json, credential fields replaced with "[STRIPPED_BY_MIGRATION]", non-credential fields and workspace files preserved, and temp cleanup.
Commit linting config
commitlint.config.js
Adds "security" to allowed commit type values in type-enum rule.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User as User
    participant MP as MigrationProcess
    participant SE as SanitizationEngine
    participant FS as FileSystem

    User->>MP: start snapshot/bundle creation
    MP->>FS: read sandbox (openclaw.json, agents/, external roots)
    MP->>SE: sanitize openclaw.json (CREDENTIAL_FIELDS)
    SE->>SE: recursively strip credential fields
    SE-->>MP: return sanitized openclaw.json
    MP->>FS: write sanitized openclaw.json
    MP->>FS: remove `agents/*/auth-profiles.json`
    MP->>FS: copy & sanitize external-root snapshots
    MP->>FS: archive sanitized bundle/snapshot
    FS-->>User: deliver sanitized snapshot/bundle
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I tiptoe through JSON, soft and bright,

I hide the tokens, tuck them out of sight.
Auth files hop away without a peep,
Openclaw wakes tidy, secrets put to sleep.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'security: strip credentials from migration snapshots and enforce blueprint digest' directly reflects the main changes in the PR: credential sanitization in migration-state.ts and test coverage. However, the blueprint digest enforcement mentioned in the title was removed during rebase because upstream modules were deleted, so the title partially overstates the current changeset scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

Copy link

@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.

🧹 Nitpick comments (3)
nemoclaw/src/commands/migration-state.ts (2)

553-561: Consider extending credential field denylist for defense in depth.

The current list covers OpenClaw's known credential patterns, but additional common field names could provide broader protection:

  • accessToken / access_token
  • privateKey / private_key
  • secretKey / secret_key
  • refreshToken / refresh_token
  • bearer, authorization, credentials

Since auth-profiles.json is deleted entirely, this is a secondary defense layer. The current list likely covers existing OpenClaw schemas, but expanding it would guard against future credential fields.

🛡️ Suggested extension
 const CREDENTIAL_FIELDS = new Set([
   "apiKey",
   "api_key",
   "token",
   "secret",
   "password",
   "resolvedKey",
   "keyRef",
+  "accessToken",
+  "access_token",
+  "privateKey",
+  "private_key",
+  "secretKey",
+  "secret_key",
+  "refreshToken",
+  "refresh_token",
+  "bearer",
+  "authorization",
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 553 - 561, Update the
CREDENTIAL_FIELDS Set used in migration-state.ts to include additional common
credential field names for defense-in-depth: add accessToken and access_token,
privateKey and private_key, secretKey and secret_key, refreshToken and
refresh_token, bearer, authorization, credentials (and any camelCase/snake_case
variants you expect). Modify the CREDENTIAL_FIELDS constant so these additional
identifiers are present alongside the existing entries to ensure the denylist
catches more potential secret fields.

627-641: Consider logging failed file removals for observability.

The empty catch block silently swallows all errors. While individual failures may be non-fatal, completely suppressing them could hide situations where credential files weren't removed due to permission issues—undermining the security intent.

🔧 Suggested improvement
-function walkAndRemoveFile(dirPath: string, targetName: string): void {
+function walkAndRemoveFile(dirPath: string, targetName: string, logger?: PluginLogger): void {
   for (const entry of readdirSync(dirPath)) {
     const fullPath = path.join(dirPath, entry);
     try {
       const stat = lstatSync(fullPath);
       if (stat.isDirectory()) {
-        walkAndRemoveFile(fullPath, targetName);
+        walkAndRemoveFile(fullPath, targetName, logger);
       } else if (entry === targetName) {
         rmSync(fullPath, { force: true });
       }
-    } catch {
-      // Non-fatal: skip files that disappeared or lack permissions
+    } catch (err: unknown) {
+      // Non-fatal but worth logging for security audit
+      logger?.warn?.(`Failed to process ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 627 - 641, The empty
catch in walkAndRemoveFile silently swallows errors; change it to capture the
exception and log a warning/error including the fullPath and error details so
failed removals are observable (use the module's logger if available, e.g.,
logger.error/ warn, otherwise console.error). Keep behavior non-fatal (do not
rethrow), but ensure the log message names the function walkAndRemoveFile and
includes dirPath/targetName context and the thrown error to aid debugging and
auditing.
test/security-credential-exposure.test.js (1)

130-147: Test duplicates implementation logic instead of testing actual functions.

This test re-implements CREDENTIAL_FIELDS and stripCredentials rather than importing them from the source module. This means:

  1. If the implementation changes, this test won't catch regressions
  2. The test has subtly different logic—Line 140 only strips strings/objects, while the source (line 576 in migration-state.ts) strips all value types unconditionally

Consider importing and testing the actual exported functions, or at minimum, ensure the test logic matches the source exactly.

🔧 Align test logic with source or import actual functions

If keeping inline logic, match the source exactly:

         for (const [key, value] of Object.entries(obj)) {
-          if (CREDENTIAL_FIELDS.has(key) && (typeof value === "string" || typeof value === "object")) {
+          if (CREDENTIAL_FIELDS.has(key)) {
             result[key] = "[STRIPPED_BY_MIGRATION]";
           } else {
             result[key] = stripCredentials(value);
           }
         }

Alternatively, if stripCredentials and sanitizeCredentialsInBundle were exported, the test could import and exercise them directly for better regression coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/security-credential-exposure.test.js` around lines 130 - 147, The test
re-implements CREDENTIAL_FIELDS and stripCredentials which can drift from the
source; update the test to either import the canonical definitions (e.g., import
CREDENTIAL_FIELDS, stripCredentials or sanitizeCredentialsInBundle from
migration-state.ts) and exercise those exports, or change the inline
stripCredentials to match the source behavior exactly by stripping credential
keys for all value types unconditionally (remove the current typeof check that
limits stripping to strings/objects) so test behavior matches the real
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 553-561: Update the CREDENTIAL_FIELDS Set used in
migration-state.ts to include additional common credential field names for
defense-in-depth: add accessToken and access_token, privateKey and private_key,
secretKey and secret_key, refreshToken and refresh_token, bearer, authorization,
credentials (and any camelCase/snake_case variants you expect). Modify the
CREDENTIAL_FIELDS constant so these additional identifiers are present alongside
the existing entries to ensure the denylist catches more potential secret
fields.
- Around line 627-641: The empty catch in walkAndRemoveFile silently swallows
errors; change it to capture the exception and log a warning/error including the
fullPath and error details so failed removals are observable (use the module's
logger if available, e.g., logger.error/ warn, otherwise console.error). Keep
behavior non-fatal (do not rethrow), but ensure the log message names the
function walkAndRemoveFile and includes dirPath/targetName context and the
thrown error to aid debugging and auditing.

In `@test/security-credential-exposure.test.js`:
- Around line 130-147: The test re-implements CREDENTIAL_FIELDS and
stripCredentials which can drift from the source; update the test to either
import the canonical definitions (e.g., import CREDENTIAL_FIELDS,
stripCredentials or sanitizeCredentialsInBundle from migration-state.ts) and
exercise those exports, or change the inline stripCredentials to match the
source behavior exactly by stripping credential keys for all value types
unconditionally (remove the current typeof check that limits stripping to
strings/objects) so test behavior matches the real implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5edec13d-48bd-4bac-bd86-ff284e7cd658

📥 Commits

Reviewing files that changed from the base of the PR and between 1dbf82f and 77fbc62.

📒 Files selected for processing (2)
  • nemoclaw/src/commands/migration-state.ts
  • test/security-credential-exposure.test.js

gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 21, 2026
C-2: Dockerfile CHAT_UI_URL Python code injection
  - Build-arg was interpolated directly into `python3 -c` source string
  - A single-quote in the URL closes the Python literal, allowing
    arbitrary code execution at image build time
  - Fix: promote to ENV, read via os.environ['CHAT_UI_URL']
  - Also fixes identical pattern for NEMOCLAW_MODEL
  - 11 tests: PoC proving injection, fix verification, regression guards

C-3: Telegram & Discord always-on in baseline sandbox policy
  - Both messaging APIs had no binaries: restriction in baseline
  - Any sandbox process could POST to attacker-controlled bots/webhooks
  - Fix: remove from baseline; opt-in presets already exist
  - 7 tests: host deny-list, binaries coverage invariant, preset existence

C-4: Snapshot manifest path traversal (arbitrary host write)
  - restoreSnapshotToHost() used manifest.stateDir/configPath as write
    targets with no path validation
  - Tampered snapshot.json could overwrite arbitrary host files
  - Fix: validate paths within manifest.homeDir via isWithinRoot()
  - 9 tests: PoC proving traversal, fix rejection, legitimate path success

Also updates audit report to mark C-1 as tracked by PR NVIDIA#156.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 21, 2026
10 parallel security scans across the entire NemoClaw codebase (~9,600 LoC)
identified 3 CRITICAL, 25 HIGH, 28 MEDIUM, and 16 LOW severity findings.

Critical findings (fixed in companion PRs):
- C-2: CHAT_UI_URL Python code injection in Dockerfile
- C-3: Telegram/Discord always-on in baseline policy (exfil channels)
- C-4: Snapshot manifest path traversal (arbitrary host write)
- C-1: Migration credential exposure (tracked by PR NVIDIA#156)

Also adds DRAFT-*.md to .gitignore to prevent accidental disclosure.
@gn00295120 gn00295120 requested a review from Copilot March 22, 2026 00:07
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the migration snapshot/bundle flow to prevent host credentials (API keys/tokens) from being copied into the sandbox, and adds regression tests demonstrating the prior exposure and the intended sanitization behavior.

Changes:

  • Add bundle sanitization in migration-state.ts to remove auth-profiles.json and strip credential-like fields from openclaw.json.
  • Sanitize external root snapshots by removing auth-profiles.json before archiving/sending into the sandbox.
  • Add a security-focused test suite covering credential exposure and sanitization.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
nemoclaw/src/commands/migration-state.ts Adds credential stripping/removal during bundle preparation and external root snapshotting.
test/security-credential-exposure.test.js Adds tests demonstrating the credential exposure PoC and verifying sanitization outcomes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@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

🧹 Nitpick comments (1)
nemoclaw/src/commands/migration-state.ts (1)

553-561: Consider expanding credential field coverage.

The allowlist covers common cases, but may miss other credential-bearing fields. Consider adding:

  • accessToken, access_token, refreshToken, refresh_token
  • privateKey, private_key
  • clientSecret, client_secret
  • bearer, bearerToken
  • credentials, auth

Alternatively, consider a pattern-based approach (e.g., matching fields ending in Key, Token, Secret) as a defense-in-depth measure, though this risks false positives.

💡 Proposed expansion
 const CREDENTIAL_FIELDS = new Set([
   "apiKey",
   "api_key",
   "token",
   "secret",
   "password",
   "resolvedKey",
   "keyRef",
+  "accessToken",
+  "access_token",
+  "refreshToken",
+  "refresh_token",
+  "privateKey",
+  "private_key",
+  "clientSecret",
+  "client_secret",
+  "bearerToken",
+  "bearer_token",
+  "credentials",
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 553 - 561, The
CREDENTIAL_FIELDS Set in migration-state.ts is too narrow and may miss other
sensitive keys; update the CREDENTIAL_FIELDS constant to include additional
common names such as accessToken, access_token, refreshToken, refresh_token,
privateKey, private_key, clientSecret, client_secret, bearer, bearerToken,
credentials, and auth, and optionally add a defensive pattern-based check (e.g.,
regexp tests in the same module for field names ending with
Key|Token|Secret|SecretKey) to catch variants—modify references that use
CREDENTIAL_FIELDS (look for usages of the CREDENTIAL_FIELDS Set) to consult the
expanded allowlist or the new pattern matcher so credential-bearing fields are
consistently detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 612-615: sanitizeExternalRootSnapshot currently only deletes
auth-profiles.json; update it to also locate config files in the external root
(e.g., openclaw.json and any other bundle config JSONs) and apply the existing
stripCredentials(...) routine to those files before writing them back, while
keeping the walkAndRemoveFile(rootSnapshotDir, "auth-profiles.json") behavior;
locate sanitizeExternalRootSnapshot and call stripCredentials on each discovered
config file (read -> stripCredentials -> write) rather than only removing
auth-profiles.json so embedded credential fields are sanitized.

---

Nitpick comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 553-561: The CREDENTIAL_FIELDS Set in migration-state.ts is too
narrow and may miss other sensitive keys; update the CREDENTIAL_FIELDS constant
to include additional common names such as accessToken, access_token,
refreshToken, refresh_token, privateKey, private_key, clientSecret,
client_secret, bearer, bearerToken, credentials, and auth, and optionally add a
defensive pattern-based check (e.g., regexp tests in the same module for field
names ending with Key|Token|Secret|SecretKey) to catch variants—modify
references that use CREDENTIAL_FIELDS (look for usages of the CREDENTIAL_FIELDS
Set) to consult the expanded allowlist or the new pattern matcher so
credential-bearing fields are consistently detected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dac7d3f9-cb1f-4599-ae10-aafcd9779ad7

📥 Commits

Reviewing files that changed from the base of the PR and between 2ea575d and 95af20d.

📒 Files selected for processing (1)
  • nemoclaw/src/commands/migration-state.ts

Fixes a critical vulnerability where createSnapshotBundle() copies the
entire ~/.openclaw directory — including auth-profiles.json with live
API keys, GitHub PATs, and npm tokens — into the sandbox filesystem.

A compromised agent can read these credentials and exfiltrate them.

Fix: Two sanitization layers in migration-state.ts:
- sanitizeCredentialsInBundle(): deletes auth-profiles.json from agents/
  subtree, strips credential fields from openclaw.json
- sanitizeExternalRootSnapshot(): strips auth-profiles.json from
  external root snapshots before archiving

Note: Blueprint digest bypass fix (verify.ts/resolve.ts) was dropped
from this PR — those modules were deleted upstream in PR NVIDIA#492 as dead
code when the openclaw nemoclaw CLI commands were removed.


Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
- Move readdirSync inside try/catch to handle permission errors
- Add { force: true } to rmSync for race-condition safety
- Log warnings instead of silently swallowing removal failures
- Remove unused node:crypto import from test


Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch from 95af20d to d3b47f6 Compare March 22, 2026 00:52
Copy link

@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: 2

♻️ Duplicate comments (1)
nemoclaw/src/commands/migration-state.ts (1)

612-615: ⚠️ Potential issue | 🔴 Critical

Replace the filename sweep with config-aware external-root sanitization.

Line 614 only deletes auth-profiles.json. That still leaves credential-bearing openclaw.json or similar config files inside copied agentDir / workspace / skillsExtraDir roots, so secrets can still reach the sandbox. It is also over-broad for arbitrary workspaces because any unrelated file with that name gets dropped. Reuse stripCredentials() for known OpenClaw config files here and keep hard deletes narrowly scoped to OpenClaw-owned auth-profile files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 612 - 615,
sanitizeExternalRootSnapshot currently just calls walkAndRemoveFile to delete
"auth-profiles.json", which is both over-broad and misses other OpenClaw
credential files; update sanitizeExternalRootSnapshot to iterate known OpenClaw
config locations (e.g., agentDir, workspace, skillsExtraDir roots inside the
provided rootSnapshotDir) and for each file call stripCredentials() to sanitize
config content for known filenames (e.g., openclaw.json and any other OpenClaw
config names), and only use walkAndRemoveFile narrowly for OpenClaw-owned
auth-profile files; reference sanitizeExternalRootSnapshot, walkAndRemoveFile,
and stripCredentials when making the change so credential-bearing configs are
sanitized rather than broadly deleted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 627-646: The walkAndRemoveFile function currently swallows all FS
errors which can leave credentials unremoved; update it so any filesystem error
from readdirSync, lstatSync, or rmSync that is not an ENOENT is re-thrown (or
converted to an Error) so caller can abort; specifically inside
walkAndRemoveFile, replace the blanket console.warn on catch with logic that
ignores only err.code === 'ENOENT' and otherwise throws the error, and ensure
callers such as createSnapshotBundle propagate that exception to abort bundle
creation rather than returning success.
- Around line 553-561: CREDENTIAL_FIELDS currently includes "keyRef" but that
value is a runtime reference (metadata) not a secret; remove "keyRef" from the
CREDENTIAL_FIELDS Set so only actual secret fields like "resolvedKey" (and the
other keys) are stripped during migration-state processing; update any related
logic that relies on CREDENTIAL_FIELDS (e.g., code that strips credentials) to
preserve keyRef so credential injection can restore secrets at runtime.

---

Duplicate comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 612-615: sanitizeExternalRootSnapshot currently just calls
walkAndRemoveFile to delete "auth-profiles.json", which is both over-broad and
misses other OpenClaw credential files; update sanitizeExternalRootSnapshot to
iterate known OpenClaw config locations (e.g., agentDir, workspace,
skillsExtraDir roots inside the provided rootSnapshotDir) and for each file call
stripCredentials() to sanitize config content for known filenames (e.g.,
openclaw.json and any other OpenClaw config names), and only use
walkAndRemoveFile narrowly for OpenClaw-owned auth-profile files; reference
sanitizeExternalRootSnapshot, walkAndRemoveFile, and stripCredentials when
making the change so credential-bearing configs are sanitized rather than
broadly deleted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 764deb5a-3d9a-4add-a86f-0f9028927734

📥 Commits

Reviewing files that changed from the base of the PR and between 95af20d and d3b47f6.

📒 Files selected for processing (3)
  • commitlint.config.js
  • nemoclaw/src/commands/migration-state.ts
  • test/security-credential-exposure.test.js
✅ Files skipped from review due to trivial changes (2)
  • commitlint.config.js
  • test/security-credential-exposure.test.js

…hots

sanitizeExternalRootSnapshot only removed auth-profiles.json but did not
strip credential fields from openclaw.json files that may exist in
external roots (agentDir, workspace, skills). Add walkAndStripCredentials
to apply the same field-level sanitization used for the main bundle.

Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch from 03c80da to 612a857 Compare March 22, 2026 01:19
…symlink guards

- Remove keyRef from CREDENTIAL_FIELDS (metadata, not a secret)
- Add CREDENTIAL_FIELD_PATTERN for broader credential detection
  (accessToken, refreshToken, privateKey, clientSecret, etc.)
- Use JSON5.parse instead of JSON.parse for openclaw.json consistency
- Add symlink guards in walkAndStripCredentials and walkAndRemoveFile
  to prevent modifying/deleting files outside the snapshot boundary
- Rewrite tests to align with production logic, add pattern matching,
  symlink safety and value-type coverage tests
Copy link
Contributor

@cv cv left a comment

Choose a reason for hiding this comment

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

Minor: three missing docstrings are causing the lint (docstring coverage) check to fail. Suggestions below should fix it.

}
for (const entry of entries) {
const fullPath = path.join(dirPath, entry);
try {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing docstring — causing the docstring coverage lint failure.

Suggested change
try {
/** Recursively walk dirPath and remove any files matching targetName. */
function walkAndRemoveFile(dirPath: string, targetName: string): void {

const stateDir = path.join(tmpDir, ".openclaw");
fs.mkdirSync(stateDir, { recursive: true });

const config = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing docstring — contributes to the docstring coverage lint failure.

Suggested change
const config = {
/** Create a mock ~/.openclaw directory tree populated with fake credential files. */
function createMockOpenClawHome(tmpDir) {

}
}
return result;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing docstring — contributes to the docstring coverage lint failure.

Suggested change
}
/** Local reimplementation of stripCredentials for test isolation. */
function stripCredentials(obj) {

Address PR review feedback from cv: add JSDoc comments to
walkAndRemoveFile, createMockOpenClawHome, and stripCredentials.
@wscurran wscurran requested a review from drobison00 March 23, 2026 16:43
ericksoa added a commit that referenced this pull request Mar 23, 2026
…print digest

1. Filter auth-profiles.json from snapshot bundles during createSnapshotBundle()
   using cpSync's filter option to exclude credential-sensitive basenames.

2. Strip gateway config (contains auth tokens) from sandbox openclaw.json
   in prepareSandboxState() — sandbox entrypoint regenerates it at startup.

3. Add blueprint digest verification: record SHA-256 of blueprint.yaml in
   SnapshotManifest at snapshot time, validate on restore. Empty/missing
   digest is a hard failure; old snapshots without the field skip validation
   for backward compatibility.

Bump SNAPSHOT_VERSION 2→3 for the new manifest field.

Closes #156
@ericksoa ericksoa self-assigned this Mar 23, 2026
@cv
Copy link
Contributor

cv commented Mar 24, 2026

This overlaps significantly with #743 (ericksoa) but takes a different approach:

Neither is a strict superset of the other. #743 misses credential fields that aren't auth-profiles.json or gateway.auth.token (e.g., provider API keys in config). #156 misses blueprint digest verification.

These should be reconciled — either merge the pattern-based stripping from #156 into #743, or coordinate so they don't conflict. Both modify migration-state.ts and its tests heavily.

gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 24, 2026
… blueprint digest

Reconciles NVIDIA#156 and NVIDIA#743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from NVIDIA#743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from NVIDIA#156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Attack chain (now broken at multiple points):

  Telegram message → shell injection → read auth-profiles.json
                                       ↑ BLOCKED: filtered at copy
  Telegram message → shell injection → read openclaw.json credentials
                                       ↑ BLOCKED: stripped by pattern
  Blueprint tampering → digest: "" → verification passes
                                     ↑ BLOCKED: hard failure

Supersedes NVIDIA#156 and NVIDIA#743.
@gn00295120
Copy link
Contributor Author

Thanks for the analysis — agreed that neither is a strict superset.

I've opened #769 which reconciles both approaches into a single PR:

53 unit tests (41 existing + 12 new), 319/319 full suite pass.

#769

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

Labels

Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. Migration Use this label to identify issues with migrating to NemoClaw from another framework. priority: high Important issue that should be resolved in the next release security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants