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
- Author identity is self-declared (
x-graft-author). Everyone holding the write token is trusted equally. Use one deployment per trust boundary until per-token identities land. - No rate limiting. Put the server behind a reverse proxy with limits if untrusted clients can reach it.
- Regex search runs on the server thread. Inputs are capped (1,000-char pattern, 20,000-char lines) but a hostile token holder can still burn CPU.