Tools send parameters the API ignores; fence untrusted text beside write tools; no unauthenticated HTTP transports #3

Closed
opened 2026-09-15 21:23:46 +00:00 by tiagoagueda · 0 comments
Owner

Found in the 2026-09-15 audit of the plugin repositories. Good already: the server is read-only by default, writes need both --write and the write scope, there are no delete tools, and redirects are not followed.

1. Two read tools send query parameters the API ignores

  • server.py:69-72: search_postings sends shortlisted / undecided, but the core's /listings takes only state (default undecided). Asking for shortlisted postings returns the undecided ones.
  • server.py:90-92: list_interviews sends upcoming, but /interviews takes state (default upcoming), so past interviews can never be listed.
  • tests/test_server.py uses an in-memory fake that accepts any parameter, which is why this passed.

Fix:

  • Map the tools to state= values.
  • Build the test fake from the core's OpenAPI schema (a checked-in openapi.json snapshot is fine) and fail on unknown parameters.
  • List endpoints are paginated and the tools fetch only the first page: expose page, or loop.

2. Untrusted text reaches the model unmarked while write tools are loaded

  • _as_text (server.py:197-213) flattens records, including captured descriptions, timeline bodies and IMAP-sourced notes, with no delimiters.
  • change_status (for example to withdrawn), add_note and create_cover_letter_draft are registered alongside (:108-142).
  • The only defence is prose in INSTRUCTIONS.

Fix:

  • Wrap third-party text in explicit untrusted-content fences.
  • Set MCP ToolAnnotations: readOnlyHint on reads, destructiveHint on change_status, so clients ask before running them.
  • Consider --write=notes,reminders, so status changes can stay off.
  • Recommend a dedicated token per agent in the README.

3. HTTP transports run with no authentication

  • __main__.py:37: --transport sse|streamable-http runs without auth or transport security. Anyone who can reach the port gets the power of the owner's token.
  • The SDK's default bind address has not been verified.
  • __main__.py:47 always passes read_only=not options.write, so POSTULO_MCP_READ_ONLY (client.py:44-46) does nothing.
  • POSTULO_MCP_INSECURE turns TLS verification off (client.py:51) and is not documented.

Fix:

  • Refuse HTTP transports unless a bearer token is configured, or bind to 127.0.0.1 with DNS-rebinding protection.
  • Honour or remove the read-only environment flag.
  • Document POSTULO_MCP_INSECURE with a warning, or remove it.

4. Version pin

As in the other plugin repositories, __version__ (0.1.0) does not match pyproject.toml (0.3.0), and the core is pinned to a main commit. Version from metadata, and pin to the release.

Found in the 2026-09-15 audit of the plugin repositories. Good already: the server is read-only by default, writes need both `--write` and the `write` scope, there are no delete tools, and redirects are not followed. ## 1. Two read tools send query parameters the API ignores - `server.py:69-72`: `search_postings` sends `shortlisted` / `undecided`, but the core's `/listings` takes only `state` (default `undecided`). Asking for shortlisted postings returns the undecided ones. - `server.py:90-92`: `list_interviews` sends `upcoming`, but `/interviews` takes `state` (default `upcoming`), so past interviews can never be listed. - `tests/test_server.py` uses an in-memory fake that accepts any parameter, which is why this passed. **Fix:** - Map the tools to `state=` values. - Build the test fake from the core's OpenAPI schema (a checked-in `openapi.json` snapshot is fine) and fail on unknown parameters. - List endpoints are paginated and the tools fetch only the first page: expose `page`, or loop. ## 2. Untrusted text reaches the model unmarked while write tools are loaded - `_as_text` (`server.py:197-213`) flattens records, including captured descriptions, timeline bodies and IMAP-sourced notes, with no delimiters. - `change_status` (for example to `withdrawn`), `add_note` and `create_cover_letter_draft` are registered alongside (`:108-142`). - The only defence is prose in `INSTRUCTIONS`. **Fix:** - Wrap third-party text in explicit untrusted-content fences. - Set MCP `ToolAnnotations`: `readOnlyHint` on reads, `destructiveHint` on `change_status`, so clients ask before running them. - Consider `--write=notes,reminders`, so status changes can stay off. - Recommend a dedicated token per agent in the README. ## 3. HTTP transports run with no authentication - `__main__.py:37`: `--transport sse|streamable-http` runs without `auth` or transport security. Anyone who can reach the port gets the power of the owner's token. - The SDK's default bind address has not been verified. - `__main__.py:47` always passes `read_only=not options.write`, so `POSTULO_MCP_READ_ONLY` (`client.py:44-46`) does nothing. - `POSTULO_MCP_INSECURE` turns TLS verification off (`client.py:51`) and is not documented. **Fix:** - Refuse HTTP transports unless a bearer token is configured, or bind to `127.0.0.1` with DNS-rebinding protection. - Honour or remove the read-only environment flag. - Document `POSTULO_MCP_INSECURE` with a warning, or remove it. ## 4. Version pin As in the other plugin repositories, `__version__` (`0.1.0`) does not match `pyproject.toml` (`0.3.0`), and the core is pinned to a `main` commit. Version from metadata, and pin to the release.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo-mcp#3
No description provided.