Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- **A tool call's arguments may hold at most 100,000 array elements and object keys, on every transport.** The MCP SDK can count them before it validates the arguments, but only when the server sets a limit, and Iris set none ([GHSA-46jr-4wx8-v453](https://github.com/advisories/GHSA-46jr-4wx8-v453), high). Arguments that fail validation are reported element by element, so one 1 MB request of 524,229 elements (zeros where `spans` belong) cost the server 260 MB, and 20 requests of that size at once, the MCP endpoint's per-minute limit for one client, up to 1,148 MB and 41 s to answer them. A call over the limit now gets an `isError` result that names the limit, before its arguments are validated, and the session goes on. The default clears every legitimate input measured, even one that fills the whole 1 MB request: real agent transcripts reach about 12,600 elements at 1 MB, and the densest shape found, a trace of 201 short tool calls, 68,506. Set it with `security.maxToolInputElements` in `config.json` or `IRIS_MAX_TOOL_INPUT_ELEMENTS`. A server reachable beyond loopback can go lower: at 10,000 the same burst costs about 250 MB. `scripts/bench-tool-input-elements.ts` measures it.
- **Iris requires `@modelcontextprotocol/sdk` 1.32.0 or later.** It required 1.30.0. A published package does not carry Iris's lockfile, so a project that already held SDK 1.30.x or 1.31.x kept it, and those versions are affected by [GHSA-46jr-4wx8-v453](https://github.com/advisories/GHSA-46jr-4wx8-v453), [GHSA-22jm-h49p-29qw](https://github.com/advisories/GHSA-22jm-h49p-29qw), [GHSA-rvq5-wwqv-78pq](https://github.com/advisories/GHSA-rvq5-wwqv-78pq) and [GHSA-6prh-2h8m-c8cw](https://github.com/advisories/GHSA-6prh-2h8m-c8cw), and 1.30.x also by [GHSA-6qxp-vccf-f47h](https://github.com/advisories/GHSA-6qxp-vccf-f47h) and, at 1.30.0, [GHSA-f897-62hf-6gxq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-f897-62hf-6gxq). The element limit above also needs 1.32.0: an older SDK ignores the option. Iris's own lockfile already installed 1.32.0.
- **`ip-address` 10.7.4** ([GHSA-rxp7-6hxx-x8q5](https://github.com/advisories/GHSA-rxp7-6hxx-x8q5), moderate: `Address6` strips a zone identifier in quadratic time before applying its length limit). Iris reaches it only through `express-rate-limit`, which parses the request's own socket address: Iris leaves Express's `trust proxy` off, so a client cannot hand it a long string. The `overrides` floor is raised from 10.5.1 to 10.7.4.
- **With an allowlist set, the citation fetcher resolves only the hosts on it.** `IRIS_CITATION_DOMAINS` is now checked before the DNS lookup, so a host that an output cites and the list does not hold is refused with `not_allowed_domain` and never looked up. It applies at every redirect hop. A host that is both outside the list and unsafe, such as a private address, now reports `not_allowed_domain` where it reported `ssrf`.
- **BREAKING — A tool argument can no longer widen what the operator allows for fetching or spend.** Three arguments could. `verify_citations` with `allow_fetch: true` fetched the cited URLs on a server whose operator had not set `IRIS_CITATION_ALLOW_FETCH=1`; its `domain_allowlist` was added to the operator's `IRIS_CITATION_DOMAINS` instead of narrowing it; and `max_cost_usd` on `evaluate_with_llm_judge` and `max_cost_usd_total` on `verify_citations` replaced the operator's cap with any larger number. An agent's arguments can be steered by text it read, including text stored in Iris, so each is now a ceiling: an argument can narrow the operator's setting for one call and never widen it. When one asks for more, the operator's setting applies and the call goes on, the response carries a warning with code `IRIS_ARGUMENT_NARROWED` naming the argument and the setting that applied, and the stored evaluation keeps the same fact (`provenance.narrowed`) with a sentence to the operator in its `interpretations`, on every later read. Fetching now needs `IRIS_CITATION_ALLOW_FETCH=1` on the server; a caller that relied on `allow_fetch: true` gets no fetch and the warning. The per-call cap on `verify_citations` is a new setting, `IRIS_CITATION_MAX_COST_USD_TOTAL`, default 1 USD as before.
- **BREAKING — Every judge call on your key draws on one daily budget.** `evaluate_with_llm_judge` and `verify_citations` had a per-call cap and no daily limit, so an agent calling either in a loop, or steered into doing so, could spend the key without end; only the relevance judge had a daily budget. All three now share one, `IRIS_LLM_JUDGE_DAILY_BUDGET_USD` (default 1 USD per UTC day, per tenant, kept in the database). A call is made only if its worst case fits in what is left. When it does not, nothing is spent: `evaluate_with_llm_judge` answers `IRIS_BUDGET_EXCEEDED` (retryable, with the time the budget resets), and `verify_citations` marks the citation `daily_budget_reached` and makes no further call. A deployment that spends more than 1 USD a day on the judge tools raises the budget. The variable was `IRIS_RELEVANCE_JUDGE_DAILY_BUDGET_USD`; that name is still read when the new one is unset, and the server says so at startup. In the Claude Desktop extension the setting is now "LLM judge daily budget", and a value set under the old one is not carried over. Today's spend is `judge.dailyBudget` on `iris://capabilities`.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ Every variable `--help` documents. CLI flags take precedence over environment va
| `IRIS_API_KEY_FILE` | Path to a file whose trimmed contents are the API key — the secret-file pattern Docker and Kubernetes mount, so the key never sits in an environment block. Set this or `IRIS_API_KEY`, not both |
| `IRIS_ALLOW_UNAUTHENTICATED` | Set to `1` to run a non-loopback bind with **no** key on purpose (lifts the refusal; the network is then your boundary) |
| `IRIS_ALLOWED_ORIGINS` | Comma-separated origin allowlist. Dashboard: CORS headers (supports globs, e.g. `http://localhost:*`). HTTP transport: exact-match `Origin` allowlist for DNS-rebinding protection (globs ignored; the server's own loopback origins are always allowed) |
| `IRIS_MAX_TOOL_INPUT_ELEMENTS` | The most array elements and object keys one tool call's arguments may hold, on every transport (a whole number, default `100000`). A call over it gets an error result before its arguments are validated. The default clears every legitimate input measured, even one that fills the whole 1 MB request; raise it if you raise `security.requestSizeLimit` for denser data. Also `security.maxToolInputElements` in `config.json` |
| `IRIS_NO_AUTO_LAUNCH` | Set to `1` to disable the first-run dashboard auto-launch |
| `IRIS_ANTHROPIC_API_KEY` | Required by `evaluate_with_llm_judge` + `verify_citations` with `provider=anthropic` |
| `IRIS_OPENAI_API_KEY` | Required by `evaluate_with_llm_judge` + `verify_citations` with `provider=openai` |
Expand Down Expand Up @@ -509,6 +510,7 @@ When using HTTP transport, Iris includes:
- Zod input validation on all routes
- ReDoS-safe regex for custom eval rules
- One 1MB request size limit on every transport (`security.requestSizeLimit`): HTTP answers `413`, stdio answers a JSON-RPC error and keeps the session open
- One limit of 100,000 array elements and object keys in a tool call's arguments on every transport (`security.maxToolInputElements`), counted before the arguments are validated: a call over it gets an error result

```bash
# Production deployment
Expand Down
11 changes: 11 additions & 0 deletions SECURITY-EXPOSURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,17 @@ All four are transitive-only (`brace-expansion` via dev-tooling globs, `fast-uri

**Gate after:** `npm audit` reports 0 vulnerabilities in the root, `dashboard/` and `website/`.

### 2026-10-22 — `ip-address` patched by override and lockfile bump (Linux-regenerated); the MCP SDK floor raised to 1.32.0 and its tool-input element limit turned on

| Advisory(ies) | Package | Was → Now | Load path | Decision |
|---|---|---|---|---|
| [GHSA-rxp7-6hxx-x8q5](https://github.com/advisories/GHSA-rxp7-6hxx-x8q5) (moderate — `Address6` strips a zone identifier in quadratic time before its length limit; 100,000 `%` characters and a newline take 3.9 s) | `ip-address` | 10.7.2 → 10.7.4 | root ← `express-rate-limit@8.7.0` — runtime: shipped in the npm package, the image and the MCPB bundle. **Not reachable from a client:** `express-rate-limit` constructs `Address6` only in `ipKeyGenerator`, from `req.ip`, after `net.isIPv6()` accepts it. iris leaves Express's `trust proxy` off (`grep -rn "trust proxy" src`: no hits), so `req.ip` is the socket's remote address as the operating system reports it, never a string a client sends. iris's source does not import `ip-address`. | **Patch** — `overrides.ip-address` raised from `^10.5.1` to `^10.7.4` so the floor excludes the advisory, and the lockfile moved with it |
| [GHSA-46jr-4wx8-v453](https://github.com/advisories/GHSA-46jr-4wx8-v453) (high — one HTTP request within the default size limit could exhaust server memory during validation), and for the floor [GHSA-22jm-h49p-29qw](https://github.com/advisories/GHSA-22jm-h49p-29qw), [GHSA-rvq5-wwqv-78pq](https://github.com/advisories/GHSA-rvq5-wwqv-78pq), [GHSA-6prh-2h8m-c8cw](https://github.com/advisories/GHSA-6prh-2h8m-c8cw), [GHSA-6qxp-vccf-f47h](https://github.com/advisories/GHSA-6qxp-vccf-f47h), [GHSA-f897-62hf-6gxq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-f897-62hf-6gxq) | `@modelcontextprotocol/sdk` | range `^1.30.0` → `^1.32.0` (the lockfile already installed 1.32.0) | root, a direct runtime dependency. iris imports `server/mcp.js`, `server/stdio.js`, `server/streamableHttp.js` and `types.js` only, so of the six only GHSA-46jr-4wx8-v453 (tool-input validation in `McpServer`) and GHSA-f897-62hf-6gxq (HTTP body and batch size; iris's own `express.json` 1 MB limit runs before the SDK sees a body) touch code iris runs; the bearer-auth, tasks and client advisories are in modules iris never loads. | **Patch** — the floor, because a published package does not carry iris's lockfile and an install that already held 1.30.x or 1.31.x kept it; and an iris-side change, because GHSA-46jr-4wx8-v453's fix is an `McpServer` option that is off by default: `security.maxToolInputElements` (default 100,000, `IRIS_MAX_TOOL_INPUT_ELEMENTS`) is now set on every MCP server iris builds, stdio and HTTP. The measurements behind the number are in `src/config/defaults.ts`; tests: `tests/integration/tool-input-elements.test.ts` (five of its six fail without the option) |

**Lockfile provenance.** Regenerated on Linux (`node:24`, npm 11.19.0) with `npm install --package-lock-only --ignore-scripts --no-audit --no-fund` after the two `package.json` edits. The root lockfile changed in two places: the root package's declared SDK range, and the version, `resolved` and `integrity` of `ip-address`. No entry added or removed (`git diff origin/main -- package-lock.json | grep -cE '^-\s+"node_modules/'` = 0), `libc` fields (10) and integrity hashes (392) unchanged in number, `@emnapi` entries unchanged (0, as before).

**Gate after:** `npm audit` at the root reports only GHSA-86w9-cpqp-85rv (`node-forge`, recorded above).


### Untrusted JSON Schema — the tool-argument path (v0.11.0+)

Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ Since 0.9.0 the server is agent-native in three ways, all locked by `tests/integ

- **Instructions.** The `initialize` response carries server instructions built at boot from this server's runtime state: the rule count and bundles, the effective critical list after `eval.criticalRules` / `eval.nonCriticalRules`, the pass threshold, whether a judge key reached the process, and the resources. A client that shows instructions gets the frame before it lists a tool; a client that hides them gets the same facts from `iris://capabilities`.
- **Structured responses.** Every tool declares an `outputSchema` (its top-level fields and their types; each response is validated against the full schema before it is sent, and what each field means is in `toolGuide` in `iris://capabilities`) and returns the same object as `content[0].text` (JSON) and as `structuredContent`. Responses that create something link it as a `resource_link` content item (`log_trace` → `iris://traces/{trace_id}`; the three verdict tools → `iris://evaluations/{id}` and the trace when linked; `list_rules` → `iris://proof`).
- **Structured errors.** A failure inside a tool returns `isError: true` with `{"error": {"code", "message", "recovery": [], "retryable", "field"?, "valid"?, "see"?, "kind"?, "retryAfterMs"?}}` as text and as `structuredContent`, plus a link to `iris://capabilities`. Codes: `IRIS_UNKNOWN_TRACE`, `IRIS_DUPLICATE_RULE`, `IRIS_INVALID_RULE_CONFIG`, `IRIS_JUDGE_NOT_ENABLED` (its `recovery` is the enable workflow), `IRIS_JUDGE_UNKNOWN_MODEL` (`valid` lists the models), `IRIS_BUDGET_EXCEEDED`, `IRIS_PROVIDER_ERROR` (`kind`: auth · rate_limit · bad_request · server_error · timeout · malformed_response), `IRIS_JUDGE_FAILED`, `IRIS_STORAGE_ERROR`, `IRIS_INTERNAL_ERROR`. An argument the input schema rejects never reaches the handler: the protocol layer answers with plain text that names the offending argument, the valid arguments and `IRIS_INVALID_ARGUMENT`. Every code is provoked over a real transport by `tests/unit/tools/error-codes.test.ts`, and the provoked set must equal the catalogue.
- **Structured errors.** A failure inside a tool returns `isError: true` with `{"error": {"code", "message", "recovery": [], "retryable", "field"?, "valid"?, "see"?, "kind"?, "retryAfterMs"?}}` as text and as `structuredContent`, plus a link to `iris://capabilities`. Codes: `IRIS_UNKNOWN_TRACE`, `IRIS_DUPLICATE_RULE`, `IRIS_INVALID_RULE_CONFIG`, `IRIS_JUDGE_NOT_ENABLED` (its `recovery` is the enable workflow), `IRIS_JUDGE_UNKNOWN_MODEL` (`valid` lists the models), `IRIS_BUDGET_EXCEEDED`, `IRIS_PROVIDER_ERROR` (`kind`: auth · rate_limit · bad_request · server_error · timeout · malformed_response), `IRIS_JUDGE_FAILED`, `IRIS_STORAGE_ERROR`, `IRIS_INTERNAL_ERROR`. An argument the input schema rejects never reaches the handler: the protocol layer answers with plain text that names the offending argument, the valid arguments and `IRIS_INVALID_ARGUMENT`. Arguments that hold more array elements and object keys than `security.maxToolInputElements` (`IRIS_MAX_TOOL_INPUT_ELEMENTS`, default 100,000) are refused the same way before the schema runs, on every transport, with text that says `arguments contain more than the maximum of 100000 elements`: send fewer tool calls or spans in one call, or raise the setting. Every code is provoked over a real transport by `tests/unit/tools/error-codes.test.ts`, and the provoked set must equal the catalogue.

One prompt is registered, `evaluate-my-agent` (optional argument `what`: `output` or `trace-file`): a walk of log → evaluate → read → explain, rendered from the same facts as the instructions. Clients without prompt support never see it and nothing depends on it.

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@
"overrides": {
"brace-expansion": "^5.0.12",
"fast-uri": "^3.1.8",
"ip-address": "^10.5.1",
"ip-address": "^10.7.4",
"postcss": "^8.5.25",
"@hono/node-server": "^2.1.0",
"tmp": "^0.2.6"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.0",
"ajv": "^8.17.1",
"express": "^5.1.0",
"express-rate-limit": "^8.3.2",
Expand Down
Loading
Loading