fix(sandbox): address PR #250 follow-up review — XSS hardening, eviction reconcile, doc sync (#243)

Security (must-fix):
- sandbox.controller: the anonymous GET /api/sb/:id response now sets
  X-Content-Type-Options: nosniff, a restrictive CSP, and Content-Disposition=
  attachment for any mime outside a raster-image allowlist (png/jpeg/gif/webp/
  avif). entry.mime is attacker-controlled, so an evil.svg/evil.html could
  otherwise execute script inline on the Docmost origin (stored XSS). Mirrors
  the public attachment route's hardening.

Stability:
- client.stashPage: reconcile mirrors AFTER the final document put, not only
  before it. The doc blob is the newest entry and FIFO eviction drops the
  oldest = this stash's own images, so the stored doc could reference an
  evicted blob (consumer 404) and over-report images.mirrored. A bounded loop
  now reverts doc-put-evicted mirrors, drops the stale doc blob, and re-puts
  until stable. Regenerated packages/mcp/build/.
- sandbox.controller: emit Cache-Control on the 304 branch too (ttlSeconds is
  computed before the conditional check).

Docs:
- Bump the MCP tool count 39 -> 40 across all READMEs and AGENTS.md (the
  registry now exposes exactly 40 tools).

Refactor:
- SandboxStore.asSink() centralizes the {put,has,evict} sink + uri<->id
  mapping; the embedded-MCP and in-app agent-tools wiring sites share it.

Tests:
- security headers (inline vs attachment, nosniff, CSP), 304 Cache-Control,
  putAndLink URL form, has()/remove(), asSink() round-trip, getSandboxPublicUrl
  (trailing-slash trim + APP_URL fallback), and a stash test where the doc put
  itself evicts a mirrored image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-28 19:08:06 +03:00
parent 6eb335d5e3
commit 8842bc8bf3
15 changed files with 371 additions and 87 deletions
@@ -119,15 +119,10 @@ export class McpService implements OnModuleDestroy {
// Bind the stash tool to the shared in-RAM SandboxStore. The store owns the
// anonymous-URL composition (putAndLink) and the live/evict probes the MCP
// package needs to keep its mirror counts honest under FIFO eviction; the
// package owns neither env nor the store. The sink speaks `uri`s, so the
// probes map a uri back to its id (the last path segment).
// package owns neither env nor the store. The uri↔id mapping now lives on the
// store (asSink), shared with the in-app agent-tools wiring site.
private buildSandboxConfig(): DocmostMcpConfig['sandbox'] {
const idOf = (uri: string) => uri.substring(uri.lastIndexOf('/') + 1);
return {
put: (buf, mime) => this.sandboxStore.putAndLink(buf, mime),
has: (uri) => this.sandboxStore.has(idOf(uri)),
evict: (uri) => this.sandboxStore.remove(idOf(uri)),
};
return this.sandboxStore.asSink();
}
// Service account the embedded MCP uses to talk back to this Docmost