Security

Supported versions

Only the latest minor release gets security fixes. Check the version you run with graft --version, graft-server --version, or GET /api/version.

Reporting a vulnerability

Please report privately to the maintainers rather than in a public issue. Include the version, a description and, if possible, a proof of concept. We aim to acknowledge within 3 days.

Threat model

Asset Threat Defense
Repository contents and history Unauthenticated network access Loopback-only by default; non-loopback requires GRAFT_TOKEN (≥ 16 chars); TLS at a reverse proxy
A developer's local server Malicious web pages (CSRF, DNS rebinding) JSON requires application/json (forces a CORS preflight that is never answered); cross-site Origin / Sec-Fetch-Site refused; Host allowlist when no token is set
The web viewer's token XSS through repository content Raw content is served as attachment with nosniff and a sandbox CSP; the viewer runs under script-src 'self' and escapes all data
The machine running graft A malicious or compromised server Every path from the server is validated, must be canonical, must stay inside the workspace (symlinks included); .git paths are rejected on both sides; blobs are hash-verified
Server availability Oversized or pathological requests 16 MiB JSON / 64 MiB blob limits, 120 s request timeout, regex and line-length caps on search, bounded diff memory

Audit — v0.2.0 (2026-11-12)

A full review of every network-facing module (server, client, CLI, MCP server, dashboard, Docker setup) plus npm audit. Each finding has a regression test in test/security.test.ts (S1–S8) or test/regression.test.ts.

ID Severity Area Finding Fix
S1 High server Cross-site request forgery. With no token configured, any web page could make the browser POST a text/plain body (no CORS preflight) that the server parsed as JSON, so visiting a page could create and land commits on a developer's local server. JSON bodies require content-type: application/json, blob uploads require application/octet-stream; requests a browser marks cross-site (Origin, Sec-Fetch-Site) are refused.
S2 High server Open by default + DNS rebinding. The server listened on 0.0.0.0 with no authentication, and answered any Host, so a hostile domain re-pointed at 127.0.0.1 could read private code. Default bind is 127.0.0.1; listening elsewhere without GRAFT_TOKEN exits with an error (opt-out: GRAFT_ALLOW_OPEN=1); tokens must be ≥ 16 chars; without a token only loopback host names are served (GRAFT_ALLOWED_HOSTS to extend).
S3 High CLI Path traversal from the server. The CLI joined server-supplied paths onto the workspace, so a malicious server could write or delete files outside it (../, absolute paths, symlinked directories) or plant .git/hooks. Paths must be canonical and resolve inside the workspace after following symlinks; .git segments are rejected by both server and client.
S4 Medium dashboard Directory traversal. The swarm dashboard checked for .. before URL-decoding, so %2e%2e%2f read arbitrary local files. Decode first, then confine with a resolved-path check; loopback Host only.
S5 Medium server, web Same-origin content execution. Raw file and blob responses had no nosniff, and the viewer had no CSP while it keeps a token in localStorage; a sniffed HTML file from a repository could run script in the viewer's origin. Raw responses: attachment, nosniff, CSP: sandbox. Viewer: strict CSP (script-src 'self', frame-ancestors 'none'), X-Frame-Options: DENY, Referrer-Policy: no-referrer.
S6 High dependency zod < 4.6.6 (GHSA-mrhx-c7xc-m3w5): memory exhaustion when validating oversized constrained collections — reachable through MCP tool inputs. Upgraded to zod 4.6.7; npm audit reports 0 vulnerabilities.
S7 Medium Docker /data was root-owned while the container runs as node; the compose file published an open server on all host interfaces. Image creates /data owned by node; compose requires GRAFT_TOKEN, publishes on host loopback only, read-only root filesystem, all capabilities dropped, no-new-privileges.
S8 Low server Internal errors (500) echoed exception messages, which can include filesystem paths. Generic message with an error id; details only in the server log.
S9 Low server Token comparison leaked the token's length through timing. Compare SHA-256 digests in constant time.
S10 Low server 64 MiB JSON bodies parsed in memory; no request timeout. 16 MiB JSON default (GRAFT_MAX_JSON_BYTES), 64 MiB blobs (GRAFT_MAX_BLOB_BYTES), 120 s request timeout.
S11 Low client Tokens were sent over plain HTTP to remote hosts without notice. Client warns on stderr; DEPLOY.md puts TLS in front.

Accepted risks