tools/call is not audit-logged — only auth acceptance is (OWASP MCP08) #64

Closed
opened 2026-10-01 16:34:36 +00:00 by mathias · 2 comments
Owner

Found during an OWASP MCP Top 10 (2025) audit, 2026-10-01.

Gap

internal/mcp/server.go handlePOST, case "tools/call", dispatches through Registry.Dispatch and writes the result. Nothing is logged. The only security log line is internal/auth/passthrough.go's "mcp auth accepted" (caller, token_fp, path), which records that someone connected, not what they did.

So repo_delete, file_delete, branch_delete, pr_merge, tbd_ship and workflow_run_trigger leave no trail in gitea-mcp. Gitea's own logs see the API call, but attributed to the PAT owner, without the MCP session, the tool name or the arguments the model chose.

Wanted

One structured slog line per tools/call: tool name, caller (auth.Caller(ctx)), token fingerprint, outcome (ok / error code), duration. Arguments: log them for mutating tools, but not file_write_branch content (size only) — an argument log must not become a second copy of repo content or secrets.

Done when

  • A test calls a mutating tool through handlePOST and asserts one log record with tool name, caller and outcome.
  • A test asserts file_write_branch content does not appear in the record.
  • Lines reach Loki (the brain's audit sink already ships there; same path).

OWASP ref: MCP08 Lack of Audit and Telemetry.


Created via git-mcp on behalf of @mathias

Found during an OWASP MCP Top 10 (2025) audit, 2026-10-01. ## Gap `internal/mcp/server.go` `handlePOST`, case `"tools/call"`, dispatches through `Registry.Dispatch` and writes the result. **Nothing is logged.** The only security log line is `internal/auth/passthrough.go`'s `"mcp auth accepted"` (caller, token_fp, path), which records *that* someone connected, not *what they did*. So `repo_delete`, `file_delete`, `branch_delete`, `pr_merge`, `tbd_ship` and `workflow_run_trigger` leave no trail in gitea-mcp. Gitea's own logs see the API call, but attributed to the PAT owner, without the MCP session, the tool name or the arguments the model chose. ## Wanted One structured `slog` line per `tools/call`: tool name, caller (`auth.Caller(ctx)`), token fingerprint, outcome (ok / error code), duration. Arguments: log them for mutating tools, but **not** `file_write_branch` content (size only) — an argument log must not become a second copy of repo content or secrets. ## Done when - A test calls a mutating tool through `handlePOST` and asserts one log record with tool name, caller and outcome. - A test asserts `file_write_branch` content does **not** appear in the record. - Lines reach Loki (the brain's audit sink already ships there; same path). OWASP ref: MCP08 Lack of Audit and Telemetry. --- _Created via git-mcp on behalf of @mathias_
mathias added the risk/lowsize/stype/feat labels 2026-10-01 16:34:36 +00:00
Author
Owner

Shipped in v0.16.0 (724a997), deployed by CD (infra 18d781c). Verified live in the pod log, 2026-10-01T18:38Z:

{"msg":"mcp tool call","tool":"pr_create","caller":"mathias","args_bytes":1479,"duration_ms":210,"token_fp":"be43a3e1dc94","owner":"mathias","repo":"skills","outcome":"ok"}
{"msg":"mcp tool call","tool":"issue_create","caller":"mathias","args_bytes":1467,"duration_ms":31,"token_fp":"be43a3e1dc94","owner":"mathias","repo":"infra","outcome":"ok"}

The PR/issue bodies are absent; only args_bytes. Tests: internal/mcp/audit_test.go (one record per call with target, error outcome, argument values other than target omitted).

Not directly verified: arrival in Loki. Records go to pod stdout like every other gitea-mcp line, so they ride the same collection path, but I did not query Loki. infra ADR-0027 names these records as the evidence source for its gateway trigger #3.

Small known gap: the legacy name/index argument aliases (#38) are not in the audit target keys, so a caller using name instead of repo logs no repo. Low impact; most callers use the canonical keys.


Created via git-mcp on behalf of @mathias

Shipped in v0.16.0 (`724a997`), deployed by CD (infra `18d781c`). Verified live in the pod log, 2026-10-01T18:38Z: ``` {"msg":"mcp tool call","tool":"pr_create","caller":"mathias","args_bytes":1479,"duration_ms":210,"token_fp":"be43a3e1dc94","owner":"mathias","repo":"skills","outcome":"ok"} {"msg":"mcp tool call","tool":"issue_create","caller":"mathias","args_bytes":1467,"duration_ms":31,"token_fp":"be43a3e1dc94","owner":"mathias","repo":"infra","outcome":"ok"} ``` The PR/issue bodies are absent; only `args_bytes`. Tests: `internal/mcp/audit_test.go` (one record per call with target, error outcome, argument values other than target omitted). Not directly verified: arrival in Loki. Records go to pod stdout like every other gitea-mcp line, so they ride the same collection path, but I did not query Loki. infra ADR-0027 names these records as the evidence source for its gateway trigger #3. Small known gap: the legacy `name`/`index` argument aliases (#38) are not in the audit target keys, so a caller using `name` instead of `repo` logs no repo. Low impact; most callers use the canonical keys. --- _Created via git-mcp on behalf of @mathias_
Author
Owner

The Loki clause I left unverified earlier is now verified. {namespace="gitea-mcp"} |= "mcp tool call" returns records, for example:

{"msg":"mcp tool call","tool":"issue_close","caller":"mathias","args_bytes":47,"duration_ms":28,"token_fp":"be43a3e1dc94","owner":"mathias","repo":"infra","number":5…}

The name/index alias gap was fixed in v0.16.1.


Created via git-mcp on behalf of @mathias

The Loki clause I left unverified earlier is now verified. `{namespace="gitea-mcp"} |= "mcp tool call"` returns records, for example: `{"msg":"mcp tool call","tool":"issue_close","caller":"mathias","args_bytes":47,"duration_ms":28,"token_fp":"be43a3e1dc94","owner":"mathias","repo":"infra","number":5…}` The `name`/`index` alias gap was fixed in v0.16.1. --- _Created via git-mcp on behalf of @mathias_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#64