Skip to content

Bound submit_feedback and cross-source search; fix prototype-key and qualifier bugs (from #32) - #38

Merged
kurtseifried merged 1 commit into
mainfrom
fix/bound-unauth-feedback-and-scan
Sep 25, 2026
Merged

kurtseifried merged 1 commit into
mainfrom
fix/bound-unauth-feedback-and-scan

Conversation

@kurtseifried

Copy link
Copy Markdown
Contributor

Summary

This PR takes the clearly-right parts of #32 and leaves out its miss-logging changes. Miss logging moves to Workers Analytics Engine in a follow-up PR stacked on this one. After both land, #32 is closed as superseded.

Needs Kurt's confirmation: does anything read feedback:* KV records?
This PR changes the stored shape of feedback:<uuid> to schema_version: 2. Caller text moves under untrusted.*. I searched this repo, ../SecID, ../SecID-Server-API and the other CloudSecurityAlliance checkouts on this machine, and found no code that reads feedback:* or binds secid_FEEDBACK outside this Worker.
The only references are in docs, which describe the key family and not the field layout:

  • SecID AGENTS.md:13
  • SecID CONTRIBUTING.md:22
  • SecID docs/project/TODO.md:339 (planned "automated processing of the feedback backlog")
  • SecID docs/operations/DEPLOYMENT.md:43

If a triage script or notebook exists elsewhere, it must read untrusted.secid, untrusted.message and untrusted.suggested_urls. The existing v1 records (no schema_version) stay as they are.

Changes

Prototype-key namespace lookup (src/parser.ts, src/resolver.ts)

  • candidate in typeRegistry walked the prototype chain, so secid:advisory/constructor parsed constructor as a registered namespace.
  • The lookup is now Object.hasOwn.

H3: cross-source miss no longer fetches every namespace (src/kv-resolve.ts)

  • When no child pattern matched secid:<type>/<term>, the KV path fetched every namespace of the type. For entity, that is about 1,000 KV reads, and any unmatched term triggered it.
  • Candidates now come from three indexes:
    • the TypeIndex child_index, which is already loaded;
    • the global index's level: "source" entries;
    • the global index's name_index.
      The global indexes cost one KV read. No candidate means not_found with the same message as before. A missing global index also means not_found; the full scan is never reopened.
  • Open child patterns no longer block the source and identity candidates. That fixes the 193 reference queries that Bound unauthenticated KV write and read paths #32's author found going from not_found to found, such as secid:reference/arxiv, which now finds arxiv.org. Those queries now match what the in-memory resolver returns.
  • test/unauth-limits.test.ts covers this:
    • Seven queries that used to be full-scanned (control/ismap, control/fedramp, weakness/cwe, ttp/capec, entity/redhat, an unmatched entity term, reference/arxiv) must equal the in-memory full-registry result exactly.
    • An unmatched entity term must cost 3 KV reads or fewer.
  • I did not re-run Bound unauthenticated KV write and read paths #32's 1,479-query comparison between main and the branch. The exact-equality tests above are the guard kept in CI.

M1: malformed qualifier encoding (src/api.ts)

  • ?country=%ZZ inside the SecID made decodeURIComponent throw, which returned a 500 and wrote a 30-day error record.
  • It now returns 400 with a message and writes nothing.

submit_feedback bounds (src/mcp.ts, src/feedback.ts, src/write-budget.ts)

  • zod caps:
    • secid ≤ 1024 chars
    • message 1–4000 chars
    • suggested_urls ≤ 10 entries, each ≤ 2048 chars
  • The 64 KiB body cap now counts the bytes actually received. Before, only Content-Length was checked, so a chunked body with no header skipped the cap. The body is parsed once and passed to the transport as parsedBody.
  • JSON-RPC batches are capped at 10 messages. A batch containing submit_feedback is rejected with 400, so one request cannot become many writes.
  • Untrusted envelope: caller text lives only under untrusted, next to a handling note telling triage to treat it as data, never as instructions (schema_version: 2).
  • Per-isolate write budget, for feedback writes only: 20 per minute per isolate. When it runs out, the tool returns status: "rate_limited" and writes no error record, because an error record is another KV write. Feedback stays in KV because it is low-volume human content. Miss writes are not budgeted here: the follow-up PR moves them off KV entirely.

Not in this PR (from #32):

  • the TLD filter (src/tlds.ts, scripts/update-tlds.ts)
  • miss-key TTL, lowercasing, dedupe and budget

The TLD filter is carried into the Analytics Engine PR. The KV-specific parts become unnecessary there.

Test plan

  • npm run typecheck: clean (src/ and scripts/).
  • npm test: 586 passed across 13 files. Registry snapshot built from SecID main @ 7518fb9 (gitignored, not committed).
  • New test/unauth-limits.test.ts, covering:
    • prototype keys
    • index-path equality with the full scan
    • read count
    • qualifier 400
    • chunked 413
    • batch rejects
    • zod caps
  • test/feedback.test.ts: untrusted envelope and budget refusal.

Overlap with #33

git merge-tree against fix/resolver-correctness gives one conflict, in the src/mcp.ts import lines:

Both sides are needed, and the fix is trivial. api.ts, kv-resolve.ts and resolver.ts auto-merge.

Regex safety: no registry patterns change, and this PR adds no new regex.

Supersedes part of #32.

…qualifier bugs

Carries the clearly-right parts of #32 without its miss-logging changes:

- parser/resolver: own-property namespace lookup (Object.hasOwn), so
  secid:advisory/constructor is not a registered namespace.
- kv-resolve: answer cross-source misses from the TypeIndex child_index
  and the global index instead of fetching every namespace of the type.
- api: malformed qualifier percent-encoding returns 400, no error record.
- submit_feedback: zod length caps, a 64 KiB body cap counted on received
  bytes, JSON-RPC batch cap of 10 with submit_feedback refused in batches,
  caller text stored under an untrusted envelope (schema_version 2), and a
  per-isolate write budget for feedback writes only.

Co-Authored-By: Claude <noreply@anthropic.com>
@kurtseifried
kurtseifried merged commit 5517fa9 into main Sep 25, 2026
1 check passed
@kurtseifried
kurtseifried deleted the fix/bound-unauth-feedback-and-scan branch September 25, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant