[Security] Hermes startup silently reprovisions state instead of refusing when /sandbox/.hermes is replaced by a symlink
- Dominant language
- TypeScript
- Stars
- 22.5k
- Forks
- 3.1k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 715
Description
## Description
Hermes startup does not refuse or warn when `/sandbox/.hermes` (the trusted Hermes configuration/state root) has been replaced by a symlink pointing to an untrusted external location. Instead it silently discards the tampered path and reprovisions a completely fresh `.hermes` directory, with no error, no security-refusal message, and no operator-visible warning. This is inconsistent with the equivalent mode-permission check on the same root, which does correctly refuse startup with an explicit message.
Related: #4957 (closed) covers a different specific trigger — a raw secret-shaped value injected into `.env` — but the same general gap (a mutated-trust-boundary condition on Hermes startup state that doesn't trigger a `[SECURITY]` refusal). This issue is about the configuration-root symlink case specifically.
Platform scope: Reproduced on Ubuntu 24.04 x86_64 only; other platforms not tested.
Regression: Unknown — earlier versions not tested.
## Environment
```text
Device: Ubuntu 24.04 x86_64 host (Docker driver)
OS: Ubuntu 24.04
Architecture: x86_64
NemoClaw: v0.0.121
```
## Steps to Reproduce
1. Onboard a Ready NemoHermes sandbox: `nemoclaw onboard --agent hermes --name hermes-sec2`
2. Stop the sandbox: `nemohermes hermes-sec2 stop`
3. As root inside the container, replace the trusted root with a symlink to an untrusted location:
```bash
docker exec -u 0 sh -c 'mv /sandbox/.hermes /sandbox/.hermes.case-backup; ln -s /tmp/hermes-root-external-case /sandbox/.hermes'
```
4. Start the sandbox: `nemohermes hermes-sec2 start`
## Expected Result
Start should exit non-zero with a security refusal identifying an unsafe linked Hermes configuration root, matching the behavior already implemented for the equivalent untrusted-mode-bits check on the same `/sandbox/.hermes` root. For comparison, that sibling scenario (root ownership/mode tampered instead of symlinked) produces this in `docker logs`:
```text
[SECURITY] Refusing Hermes startup because /sandbox/.hermes does not match a trusted owner/group/mode posture
```
followed by exit code 1.
## Actual Result
Start exits 0 and reports success (status ready, gateway healthy, probe `result=ready`). No `[SECURITY]`-prefixed message appears in `docker logs` for this startup attempt. The tampered symlink at `/sandbox/.hermes` is gone afterward (`test -L` reports not-a-symlink) and a brand-new `.hermes` directory has been silently provisioned in its place (fresh `config.yaml`, fresh `sessions` dir, fresh timestamps), discarding both the untrusted redirect and the sandbox operator's prior state/history with no warning that this happened.
The external sentinel file placed at the untrusted target was not read/modified (confirmed via unchanged sha256), so this is not a follow-the-symlink data-exposure bug — the untrusted target itself was never trusted. But the fail-closed-with-explanation contract that the mode-permission check enforces on the exact same path is not applied to the symlink case: instead of refusing and telling the operator what happened, the product silently discards their sandbox state.
## Logs
```text
docker logs (relevant excerpt): no SECURITY-prefixed line at all for this startup attempt, contrasted with the sibling mode-0755 test on the same host/build, which printed:
[SECURITY] Refusing Hermes startup because /sandbox/.hermes does not match a trusted owner/group/mode posture
followed by exit code 1. The symlink-replacement startup instead completed the full non-interactive onboarding probe successfully (result=ready) with no equivalent line.
```
Contributor guide
Research direction
Reproduce the case with `nemoclaw onboard`, `nemohermes hermes-sec2 stop`, the documented `docker exec` symlink replacement, and `nemohermes hermes-sec2 start`; compare its logs and exit status with the mode-permission scenario. Trace Hermes startup validation for `/sandbox/.hermes`; done means startup exits non-zero with a `[SECURITY]` refusal identifying the unsafe linked root instead of silently reprovisioning state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, typescript
- Domain
- infrastructure, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100