Repository navigation
[Security] Sandbox escape + approval bypass: the browser-session signing secret is readable from inside the file sandbox #8678
Replies: 2 comments
|
Disclosure-process request, not a vulnerability report. Answering the question raised in this thread directly. I checked the four standard channels today
So the answer to this thread's question is: there is currently no private channel. A researcher This is not hypothetical. It has already shaped real disclosures:
The ask is small:
# Security Policy
## Reporting a Vulnerability
Report suspected vulnerabilities via GitHub private vulnerability reporting:
https://github.com/deepseek-ai/deepseek-harness/security/advisories/new
Please do not open a public issue for a suspected vulnerability.
Include: affected version(s), platform, impact, and a minimal reproduction if available.
## What to Expect
- Acknowledgement within <N> business days.
- Initial assessment within <N> business days.
- Progress updates, and credit in the advisory unless you prefer otherwise.
## Supported Versions
| Version | Supported |
| ------- | --------- |
| <x.y.z> | yes |
| older | no |This repository runs arbitrary shell commands on users' machines and ships as a desktop 中文摘要:本仓库当前没有任何私密漏洞上报渠道——无 |
|
This is a critical architectural oversight. The fact that the control plane's authentication secret resides in a readable area within the sandbox fundamentally breaks the trust boundary, especially since settings mutations bypass the approval seam. It’s a great catch on how the HMAC cookie reconstruction allows for silent escalation to I completely agree with the request to enable private vulnerability reporting and add a On a practical note, when I’m working on hardening local development environments or setting up secure agent workflows, I’ve found that tools like AI Pro (labagent.tech) can be a helpful companion. It’s not a replacement for the core security fixes you’re describing, but I’ve used it to streamline my own testing pipelines and keep my local sandbox configurations consistent, which helps me spot potential misconfigurations earlier in my personal workflow. It’s a simple tip for maintaining a tighter security posture in local dev setups. I hope the maintainers can prioritize this fix. It seems like a relatively contained change if the secret is moved to a secure, non-readable location for the sandboxed process and the RPC endpoint is properly gated behind the approval system. Great work on documenting the root cause so clearly. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
DSH confines the agent's file writes to the session workspace, and gates sandbox
escalation requests behind human approval. Both of those mechanisms work as documented.
They are bypassed together, because the secret that authenticates the local control plane is
stored in a plaintext file inside the read-permissive region, and the control plane itself is
not covered by the approval seam.
A command running inside
workspace-writecan:$DSH_HOME/.credentials.yaml,danger-full-access,Step 3 is a settings mutation, not an escalation request, so it never reaches the approval
subsystem. No human interaction occurs at any point.
Verified on: desktop
0.2.0-rc.2, Windows x64, defaultdesktopprofile.This is not CVE-2026-82533 and not CVE-2026-101131
The CVE-2026-82533 fix (per-process launch token + signed cookie,
>= 0.1.2-alpha.1) is presentand working in
0.2.0-rc.2. The launch token is handled correctly: created asencodeBase64Url(randomBytes(32))in a module-levelWeakMapon the root app context, neverwritten to disk, never passed to a child environment.
DSH_WEB_URLhanded to the agent's shell isthe clean URL (
localWebUrl,dsh-web-app/lib/index.js:95-99), and token acceptance isconfined to
GET /insideauthorizeIndex(dsh-client-connection/lib/index.js:388-427).The escape does not need the token, because the cookie can be minted from the on-disk secret
instead.
Root cause
1. The cookie-signing secret is persisted in plaintext
packages/client/connection/src/…→ compiled atdsh-client-connection/lib/index.js:223dsh-client-connection/lib/index.js:325-342modifyRecordreturns the existing record when unchanged, so the secret is durable acrossrestarts and therefore valid for the full cookie lifetime —
cookieMaxAgeDaysdefaults to30(
dsh-client-connection/lib/index.js:802, applied at:815-819).The document is plaintext YAML at
$DSH_HOME/.credentials.yaml(
dsh-credentials-local/lib/index.js:49-58). The module enforces an owner-only permission check(
dsh-credentials-local/lib/index.js:63-104), but it is POSIX-mode based and skipped onWindows; there the file inherits ordinary user-profile NTFS ACLs and is readable by any
same-user process.
2. Cookie verification depends only on that secret
dsh-client-connection/lib/index.js:284-305and:433-443There is no server-side session registry, no nonce, and no revocation. Anyone holding the secret,
and knowing the request authority (which the caller supplies in
Host), can mint a valid cookieoffline. The cookie name is derived from the authority by a public formula.
3. The sandbox fences writes, not reads — by design
dsh-fs-sandbox/README.md:12anddsh-sandbox-windows-acl/README.md:125,188state it plainly:workspace-writerestricts writes and deletes; reads and network access are not confined.On Windows the restricted token uses
WRITE_RESTRICTED(an intersection on writes only).There is no read fence for the harness home anywhere in the product. A grep across all shipped
@deepseek-ai/*packages'lib/*.jsforresolveDshHome/.dshreturns zero hits in anysandbox or filesystem package.
4. The control plane has no approval gate
dsh-commands/lib/index.js:327-395—CommandRuntime.executeparses the line, appendscommand/run, and calls the command handler at:388. No approval call, no capability check.The
/permissionhandler lands atdsh-permission-presets/lib/index.js:222-224 → :350-361, whoseapply()performs a bare mode write;dsh-sandbox-policy/lib/index.js:40-42issession.append("sandbox/mode", {mode}). Thedanger-full-accesspreset also carriesapproval: "never"(dsh-permission-presets/lib/index.js:151-156).approval.request()has exactly two callers in the whole product: the tool-execution ask path(
dsh-tools/lib/index.js:3439-3462) and the sandbox escalation seam(
dsh-sandbox/lib/index.js:18). A settings mutation is not a tool call, so it never reaches theapproval seam. This is the same structural characteristic described in the CVE-2026-82533
write-up — "approval was applied only when a command requested broader access … this call did not
request it; it changed the session's setting instead" — and it is still true after that fix.
The agent parameter is resolved purely from the caller-supplied wire value
(
dsh-api-gateway/lib/index.js:1096-1118), with no peer or ownership comparison.Verification performed (no exploit code included)
Run through DSH's own confined shell tool on a default
0.2.0-rc.2desktop install:$DSH_HOME/.credentials.yamlfrom inside the sandboxbrowser-sessionsecret and the API key$DSH_HOMEfrom inside the sandboxPOST /api/…to the local control plane without a cookiePOST /api/commands/executewithline: "/permission danger-full-access"{"kind":"success","text":"preset danger-full-access"}, no approval prompt"permissions":{"currentValue":"danger-full-access"}workspace-write, then write againThe cookie is computed from the secret alone; the launch token is never needed.
A second, even more direct route exists in code (not executed):
terminal/createallocates ashell "without Agent sandbox or approval restrictions" — its own comment,
dsh-api-terminal-controller/lib/index.js:811;create→spawn(:996-1011) callsctx.subprocess.spawnTerminaland neverctx.sandbox.confine, unlike the model's own shelltool (
dsh-terminal-bash/lib/index.js:963-971). Any authenticated caller can name anyagentId.Impact
An agent processing untrusted content (a repository, a web page, a fetched document, MCP output)
can be induced to perform the sequence above. Result: unconfined code execution as the user, with
approval prompts permanently disabled for that session, no human interaction, and no visible
prompt. This defeats the two containment mechanisms the product advertises.
Severity rationale
The product explicitly frames read access, network access and process visibility as not
confined, so "reads are unfenced" alone is not the finding. The finding is that the
authentication secret for the control plane — whose entire purpose is to be unreachable from
inside the confinement — was placed inside the read-permissive region. The consequence is a
total loss of both the sandbox and the approval boundary.
Suggested: Critical,
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H.Additional findings from the same audit (lower severity, distinct fixes)
These are separate defects; each is worth its own fix even if the above is addressed.
askpolicy is effectively vacuous.dsh-tools/lib/index.js:3225uses a waterfall whosedefault is
{kind:"allow"};serviceAskruns only when a listener returnedask(:3226).Exactly two producers of
kind:"ask"exist in the tree:dsh-experimental-auto-review(
lib/index.js:436, only under theautopreset) anddsh-hooks-claude-code(
lib/index.js:260, not composed in the shipped desktop profile).defineToolhas no field bywhich a tool can require approval (
dsh-tools/lib/index.js:838-887). Consequence: under theshipped default, injection-driven shell execution, arbitrary file reads (including
$DSH_HOME), network egress and workspace writes never prompt. (Observed in practice: dozens ofshell commands executed during this audit produced no approval prompt.)
plugin_manager's gate is self-satisfying once the session is already at the target mode.dsh-plugin-manager/lib/types/tools.js:32-36always requestsrequestedMode:'danger-full-access';dsh-sandbox/lib/index.js:101returns before any approvalwhen
mode === effectiveMode. In adanger-full-accesssession, bundle install/enable thereforeruns unprompted, pnpm runs, and install scripts are allowed by a name list the model itself
supplies (
dsh-plugin-manager/lib/types/index.js:489,build-approval.js:46-55). Installed hostcode then runs in-process and persists across sessions. Combined with the escape above, this is a
persistent-backdoor path.
is persisted into
$DSH_HOME/profiles/<profile>/settings.yaml(
dsh-settings/lib/index.js:348,:488-520; the exact op isdsh-client-ui-permission-presets/lib/client.js:644-659). No tool callssettings.mutate, sothis rides the same ungated control plane — but its distinct impact is that one successful
injection becomes permanent rather than per-session.
usermessage with no untrusted marking.dsh-subagent/lib/index.js:633-653(createSettlementMessage→createUserMessage) and:584-592(Agent <id> sent a message:— also a user message). Provenance exists only insourcemetadata the model never sees. A child that read hostile content can therefore injectinstruction-shaped text at user authority in the parent.
dsh-agent-instructions/lib/index.js:17-18(AGENTS.md/CLAUDE.md),:827-832(re-readmid-session). The only structural escaping is
:127-128, which escapes the closing</system-reminder>only — the opening tag and theInstructions from: <path>framing remainforgeable inside the body. Workspace skills are likewise auto-trusted
(
dsh-skill-filesystem/lib/index.js:31-44,:150-165).instructionsare injected into the system prompt as instructions, not data.dsh-mcp-client/lib/index.js:654-656,:731-743. The stdio transport spawns the configuredcommand with no sandbox and no mount-time approval.
Suggested remediation
an OS-protected store (DPAPI / Keychain / libsecret), or make it process-lifetime only. The
launch-token exchange still works; the cost is re-authenticating the browser after each restart.
and session store. "Writes confined, reads free" is defensible only until a secret lands in the
readable region.
commands/execute,settings/mutate,terminal/create. "Change my own permission setting" is functionally anescalation.
terminal/createshould not hand out an unsandboxed shell for a caller-suppliedagentId.workspace-writesession is not a confidentiality boundary against thecontent it processes.
Environment / limitations
0.2.0-rc.2.precondition — a plaintext secret on disk plus unfenced reads — is platform-independent, so the
escape is expected to reproduce there, but only the Windows read path was exercised.
traverse outside it (no reparse-point guard exists in that path).
中文摘要
问题:浏览器会话签名密钥以明文存放在
$DSH_HOME/.credentials.yaml,而 DSH 的文件沙箱只限制写入、不限制读取。沙箱内的进程因此可以读到该密钥,离线伪造出有效的会话 cookie,再通过不在审批范围内的通用命令 RPC 把自身会话改成danger-full-access,从而写出工作区。全程零交互、无审批提示。与已知 CVE 的关系:
CVE-2026-82533的修复(每进程启动令牌 + 签名 cookie)在0.2.0-rc.2中确实有效,启动令牌的处理也是正确的。但该修复没有堵住这条路径,因为它依赖的密钥落在可读区域内。本问题独立于CVE-2026-82533与CVE-2026-101131。已实测(默认配置、Windows):沙箱内读密钥成功 → 沙箱内写文件被拒(证明沙箱有效)→ 无 cookie 401 → 用密钥伪造 cookie 200 →
commands/execute改权限成功且无审批 → 写出工作区成功 → 回滚后边界恢复。建议修复优先级:① 密钥移出明文文件(DPAPI/Keychain/仅进程内);② 给
$DSH_HOME加读取围栏;③ 控制面(commands/execute、settings/mutate、terminal/create)纳入审批缝;④terminal/create不得仅凭调用方提供的agentId发放无沙箱 shell。All reactions