summaryrefslogtreecommitdiff
path: root/docs/adr/003-security-model.md
blob: 529e50e8a682fefc05beaebe981a13fc7be28379 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# ADR-003: Security Model

## Status
Accepted

## Context

Claudomator is a local developer tool: it runs on the developer's own machine,
accepts tasks from the operator (a human or automation they control), and executes
`claude` subprocesses with filesystem access. The primary deployment model is
single-user, single-machine — not a shared or internet-facing service.

This ADR documents the current security posture, the explicit trust boundary
assumptions, and known risks surfaced during code review (2026-03-08).

## Trust Boundary

```
[ Operator (human / script) ]
            │  loopback or LAN
            ▼
  [ claudomator HTTP API :8484 ]
            │
  [ claude subprocess (bypassPermissions) ]
            │
  [ local filesystem, tools ]
```

**Trusted:** The operator and all callers of the HTTP API. Any caller can create,
run, delete, and cancel tasks; execute server-side scripts; and read all logs.

**Untrusted:** The content that Claude processes (web pages, code repos, user
instructions containing adversarial prompts). The tool makes no attempt to sandbox
Claude's output before acting on it.

## Explicit Design Decisions

### No Authentication or Authorization

**Decision:** The HTTP API has no auth middleware. All routes are publicly
accessible to any network-reachable client.

**Rationale:** Claudomator is a local tool. Adding auth would impose key management
overhead on a single-user workflow. The operator is assumed to control who can
reach `:8484`.

**Risk:** If the server is exposed on a non-loopback interface (the current default),
any host on the LAN — or the public internet if port-forwarded — can:
- Create and run arbitrary tasks
- Trigger `POST /api/scripts/deploy` and `POST /api/scripts/start-next-task`
  (which run shell scripts on the server with no additional gate)
- Read all execution logs and task data via WebSocket or REST

**Mitigation until auth is added:** Run behind a firewall or bind to `127.0.0.1`
only. The `addr` config key controls the listen address.

### Permissive CORS (`Access-Control-Allow-Origin: *`)

**Decision:** All responses include `Access-Control-Allow-Origin: *`.

**Rationale:** Allows the web UI to be opened from any origin (file://, localhost
variants) during development. Consistent with no-auth posture: if there is no
auth, CORS restriction adds no real security.

**Risk:** Combined with no auth, any web page the operator visits can issue
cross-origin requests to `localhost:8484` if the browser is on the same machine.

### `bypassPermissions` as Default Permission Mode

**Decision:** `executor.ClaudeRunner` defaults `permission_mode` to
`bypassPermissions` when the task does not specify one.

**Rationale:** The typical claudomator workflow is fully automated, running inside
a container or a dedicated workspace. Stopping for tool-use confirmations would
break unattended execution. The operator is assumed to have reviewed the task
instructions before submission.

**Risk:** Claude subprocesses run without any tool-use confirmation prompt.
A malicious or miscrafted task instruction can cause Claude to execute arbitrary
shell commands, delete files, or make network requests without operator review.

**Mitigation:** Tasks are created by the operator. Prompt injection in task
instructions or working directories is the primary attack surface (see below).

### Prompt Injection via `working_dir` in Elaborate

**Decision:** `POST /api/elaborate` accepts a `working_dir` field from the HTTP
request body and embeds it (via `%q`) into a Claude system prompt.

**Risk:** A crafted `working_dir` value can inject arbitrary text into the
elaboration prompt. The `%q` quoting prevents trivial injection but does not
eliminate the risk for sophisticated inputs.

**Mitigation:** `working_dir` should be validated as an absolute filesystem path
before embedding. This is a known gap; see issue tracker.

## Known Risks (from code review 2026-03-08)

| ID | Severity | Location | Description |
|----|----------|----------|-------------|
| C1 | High | `server.go:62-92` | No auth on any endpoint |
| C2 | High | `scripts.go:28-88` | Unauthenticated server-side script execution |
| C3 | Medium | `server.go:481` | `Access-Control-Allow-Origin: *` (intentional) |
| C4 | Medium | `elaborate.go:101-103` | Prompt injection via `working_dir` |
| M2 | Medium | `logs.go:79,125` | Path traversal: `exec.StdoutPath` from DB not validated before `os.Open` |
| M3 | Medium | `server.go` | No request body size limit — 1 GB body accepted |
| M6 | Medium | `scripts.go:85` | `stderr` returned to caller may contain internal paths/credentials |
| M7 | Medium | `websocket.go:58` | WebSocket broadcasts task events (cost, errors) to all clients without auth |
| M8 | Medium | `server.go:256-269` | `/api/workspaces` enumerates filesystem layout; raw `os.ReadDir` errors returned |
| L1 | Low | `notify.go:34-39` | Webhook URL not validated — `file://`, internal addresses accepted (SSRF if exposed) |
| L6 | Low | `reporter.go:109-113` | `HTMLReporter` uses `fmt.Fprintf` for HTML — XSS if user-visible fields added |
| X1 | Low | `app.js:1047,1717,2131` | `err.message` injected via `innerHTML` — XSS if server returns HTML in error |

Risks marked Medium/High are acceptable for the current local-only deployment model
but must be addressed before exposing the service to untrusted networks.

## Recommended Changes (if exposed to untrusted networks)

1. Add API-key middleware (token in `Authorization` header or `X-API-Key` header).
2. Bind the listen address to `127.0.0.1` by default; require explicit opt-in for
   LAN/public exposure.
3. Validate `StdoutPath` is under the known data directory before `os.Open`.
4. Wrap request bodies with `http.MaxBytesReader` (e.g. 10 MB limit).
5. Sanitize `working_dir` in elaborate: must be absolute, no shell metacharacters.
6. Validate webhook URLs to `http://` or `https://` scheme only.
7. Replace `fmt.Fprintf` HTML generation in `HTMLReporter` with `html/template`.
8. Replace `innerHTML` template literals containing `err.message` with `textContent`.

## Consequences

- Current posture: local-only, single-user, no auth. Acceptable for the current use case.
- Future: any expansion to shared/remote access requires auth first.
- The `bypassPermissions` default is a permanent trade-off: unattended automation vs.
  per-operation safety prompts. Operator override via task YAML `permission_mode` is
  always available.