Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions SECURITY_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: Apache-2.0 -->

# Security Notes

## Upstream vulnerability snapshot

This note records a review snapshot for OpenClaw@2026.4.11 transitive
dependencies (Lark SDK `@larksuiteoapi/node-sdk` and Discord `axios`/`tar`
deps).

**Mitigation:** The tightened baseline sandbox policy blocks direct access to
Lark and Discord endpoints by default.

**Action:** Revisit when OpenClaw ships `axios`/`tar` dependency bumps.
71 changes: 7 additions & 64 deletions nemoclaw-blueprint/policies/openclaw-sandbox.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,42 +146,13 @@ network_policies:
# discoverable preset (`presets/github.yaml`) so a sandbox only gets
# GitHub access when the user explicitly selects the `github` preset
# during onboard. See #1583.

# ── OpenClaw "phone home" ────────────────────────────────────────────
# Minimum viable set for OpenClaw to authenticate, discover plugins,
# and reach ClawHub. Restricted to openclaw and node (skill flows run on Node).
# Docs access is read-only (GET). ClawHub and openclaw.ai are
# restricted to GET+POST (auth flows, plugin discovery).

clawhub:
name: clawhub
endpoints:
- host: clawhub.ai
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
binaries:
- { path: /usr/local/bin/openclaw }
- { path: /usr/local/bin/node }

openclaw_api:
name: openclaw_api
endpoints:
- host: openclaw.ai
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
binaries:
- { path: /usr/local/bin/openclaw }
- { path: /usr/local/bin/node }
#
# SECURITY: The baseline policy intentionally omits optional hosted
# OpenClaw service-discovery endpoints such as clawhub.ai and
# openclaw.ai so the sandbox stays deny-by-default for plugin
# discovery and other "phone home" flows. Operators who need that
# access can widen egress explicitly with a different tier or policy
# override.

openclaw_docs:
name: openclaw_docs
Expand Down Expand Up @@ -230,31 +201,3 @@ network_policies:
- allow: { method: GET, path: "/file/bot*/**" }
binaries:
- { path: /usr/local/bin/node }

discord:
name: discord
endpoints:
- host: discord.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
# WebSocket gateway — must use access: full (CONNECT tunnel) instead
# of protocol: rest. The proxy's HTTP idle timeout (~2 min) kills
# long-lived WebSocket connections; a CONNECT tunnel avoids
# HTTP-level timeouts entirely. Matches presets/discord.yaml. See #409.
- host: gateway.discord.gg
port: 443
access: full
- host: cdn.discordapp.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
binaries:
- { path: /usr/local/bin/node }
32 changes: 22 additions & 10 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fi
# at startup using capsh. The bounding set limits what caps any child process
# (gateway, sandbox, agent) can ever acquire.
#
# Kept: cap_chown, cap_setuid, cap_setgid, cap_fowner, cap_kill
# Kept: cap_chown, cap_setuid, cap_setgid, cap_kill
# — required by the entrypoint for gosu privilege separation and chown.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/797
if [ "${NEMOCLAW_CAPS_DROPPED:-}" != "1" ] && command -v capsh >/dev/null 2>&1; then
Expand All @@ -110,7 +110,7 @@ if [ "${NEMOCLAW_CAPS_DROPPED:-}" != "1" ] && command -v capsh >/dev/null 2>&1;
if capsh --has-p=cap_setpcap 2>/dev/null; then
export NEMOCLAW_CAPS_DROPPED=1
exec capsh \
--drop=cap_net_raw,cap_dac_override,cap_sys_chroot,cap_fsetid,cap_setfcap,cap_mknod,cap_audit_write,cap_net_bind_service \
--drop=cap_net_raw,cap_dac_override,cap_sys_chroot,cap_fsetid,cap_setfcap,cap_mknod,cap_audit_write,cap_net_bind_service,cap_fowner \
-- -c 'exec /usr/local/bin/nemoclaw-start "$@"' -- "$@"
else
echo "[SECURITY] CAP_SETPCAP not available — runtime already restricts capabilities" >&2
Expand Down Expand Up @@ -939,15 +939,27 @@ if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then
exec gosu sandbox "${NEMOCLAW_CMD[@]}"
fi

# SECURITY: Protect gateway log from sandbox user tampering
touch /tmp/gateway.log
chown gateway:gateway /tmp/gateway.log
chmod 600 /tmp/gateway.log
# SECURITY: Protect gateway log from sandbox user tampering.
# Guard against symlink hijacking: if the path already exists as a symlink,
# an attacker could point it at a sensitive file and have the entrypoint
# overwrite it. Reject symlinks, then use mktemp+mv to avoid TOCTOU races.
for _log_path in /tmp/gateway.log /tmp/auto-pair.log; do
if [ -L "$_log_path" ]; then
echo "[SECURITY] $_log_path is a symlink — refusing to start (possible symlink hijack)" >&2
exit 1
fi
rm -f "$_log_path"
done

_gw_log="$(mktemp /tmp/gateway.log.XXXXXX)"
chmod 600 "$_gw_log"
chown gateway:gateway "$_gw_log"
mv "$_gw_log" /tmp/gateway.log

# Separate log for auto-pair so sandbox user can write to it
touch /tmp/auto-pair.log
chown sandbox:sandbox /tmp/auto-pair.log
chmod 600 /tmp/auto-pair.log
_ap_log="$(mktemp /tmp/auto-pair.log.XXXXXX)"
chmod 600 "$_ap_log"
chown sandbox:sandbox "$_ap_log"
mv "$_ap_log" /tmp/auto-pair.log
Comment on lines +954 to +962

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

touch "$tmpdir/src"
mkdir "$tmpdir/dst"

mv "$tmpdir/src" "$tmpdir/dst"
test -f "$tmpdir/dst/src"

touch "$tmpdir/src2"
if mv -T "$tmpdir/src2" "$tmpdir/dst" 2>/dev/null; then
  echo "unexpected: mv -T replaced a directory"
  exit 1
fi

echo "plain mv treats an existing directory as a directory; mv -T rejects it"

Repository: NVIDIA/NemoClaw

Length of output: 129


🏁 Script executed:

# Check if file exists and read the relevant section
fd -t f "nemoclaw-start.sh" scripts/

Repository: NVIDIA/NemoClaw

Length of output: 84


🏁 Script executed:

# Read lines around 954-962 to verify the snippet and context
sed -n '950,965p' scripts/nemoclaw-start.sh | cat -n

Repository: NVIDIA/NemoClaw

Length of output: 613


🏁 Script executed:

# Check for shebang and SPDX header at the top of the file
head -20 scripts/nemoclaw-start.sh | cat -n

Repository: NVIDIA/NemoClaw

Length of output: 1433


Use mv -T for the final log-path replace.

Lines 957 and 962 use plain mv. If the destination is recreated as a directory between the rm -f step and the rename, mv will move the temp file into that directory instead of replacing the pathname, which weakens the TOCTOU hardening here.

Suggested fix
 _gw_log="$(mktemp /tmp/gateway.log.XXXXXX)"
 chmod 600 "$_gw_log"
 chown gateway:gateway "$_gw_log"
-mv "$_gw_log" /tmp/gateway.log
+mv -T "$_gw_log" /tmp/gateway.log
@@
 _ap_log="$(mktemp /tmp/auto-pair.log.XXXXXX)"
 chmod 600 "$_ap_log"
 chown sandbox:sandbox "$_ap_log"
-mv "$_ap_log" /tmp/auto-pair.log
+mv -T "$_ap_log" /tmp/auto-pair.log
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_gw_log="$(mktemp /tmp/gateway.log.XXXXXX)"
chmod 600 "$_gw_log"
chown gateway:gateway "$_gw_log"
mv "$_gw_log" /tmp/gateway.log
# Separate log for auto-pair so sandbox user can write to it
touch /tmp/auto-pair.log
chown sandbox:sandbox /tmp/auto-pair.log
chmod 600 /tmp/auto-pair.log
_ap_log="$(mktemp /tmp/auto-pair.log.XXXXXX)"
chmod 600 "$_ap_log"
chown sandbox:sandbox "$_ap_log"
mv "$_ap_log" /tmp/auto-pair.log
_gw_log="$(mktemp /tmp/gateway.log.XXXXXX)"
chmod 600 "$_gw_log"
chown gateway:gateway "$_gw_log"
mv -T "$_gw_log" /tmp/gateway.log
_ap_log="$(mktemp /tmp/auto-pair.log.XXXXXX)"
chmod 600 "$_ap_log"
chown sandbox:sandbox "$_ap_log"
mv -T "$_ap_log" /tmp/auto-pair.log
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/nemoclaw-start.sh` around lines 954 - 962, The move operations for
temporary log files (_gw_log and _ap_log) use plain mv which can accidentally
place the temp file inside a directory if the destination path is recreated;
replace those calls with mv -T when renaming the temp files to the final paths
(/tmp/gateway.log and /tmp/auto-pair.log) so the rename always replaces the
destination pathname atomically and avoids a TOCTOU window.


# Verify ALL symlinks in .openclaw point to expected .openclaw-data targets.
# Dynamic scan so future OpenClaw symlinks are covered automatically.
Expand Down
Loading