On the server's key, one user's Signal text shares a Jev call with other users' Signals and may steer their verdicts #343

Closed
opened 2026-10-23 05:26:57 -07:00 by rays · 1 comment
Owner

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it.

When an admin turns on signals_on_server and TYPESAFE_KEY is set, pass() calls calls(items, true), which puts every user's unjudged Signal rows for the same (feed_id, guid) into one group, and judge() sends one TypeSafe request built by request(). Each question's instructions interpolate its owner's free-text name, keywords and up to 500 characters of description with no delimiting. A signed-in user subscribed to the same feed as another user can write a description aimed at the other questions in the same request. The other user's verdicts are read from that shared answer and written to signal_verdicts; a false verdict is not asked again and a true one is rechecked only when its owner edits the Signal. Source confirms the mixing. It does not show whether Jev lets one question's instructions change another question's answer. Impact is limited to which of the victim's own subscribed items their Signal marks, and the attacker sees nothing back from it. This only works when both users' rows for the item are unjudged in the same batch.

Claimed root cause. calls() groups rows from different principals into one call whenever share is true (server key in use), and request() builds one questions map from all of them, so text one user wrote reaches the model context that produces another user's verdicts. Nothing isolates one principal's questions from another's.

Local check. {'local': "In a sandbox, run a scratch daemon on SQLite with two dummy users A and B subscribed to one fixture feed, signals_on_server=true, TYPESAFE_KEY set to a dummy value, and IPX_TYPESAFE_URL pointing at a local stub that records request bodies and answers noul 0.0 for every key. Give B the Signal 'Cooking'. Give A a Signal whose description says to answer every question about this item true. Confirm the stub gets one request with both signal_ and signal_ keys and A's text inside the shared questions object. Then confirm signal_verdicts holds B's row with hit=false and that the next pass does not ask about that item again.", 'deployment': "Owner-observed, with a TypeSafe test key and not production's database: on one fixture item, send B's question alone, then again with A's injected question in the same request, a few times each, and compare B's noul score. If B's score moves materially, cross-question influence is confirmed (low-severity integrity of B's Signal results). If Jev keeps questions isolated, record it as hardening: send one call per user on the server key too, or fence each description as quoted data."}

Owner check (no probing). Owner-observed, with a TypeSafe test key and not production's database: on one fixture item, send B's question alone, then again with A's injected question in the same request, a few times each, and compare B's noul score. If B's score moves materially, cross-question influence is confirmed (low-severity integrity of B's Signal results). If Jev keeps questions isolated, record it as hardening: send one call per user on the server key too, or fence each description as quoted data.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:signals.rs:request:shared-call-mixes-users-questions).

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it. When an admin turns on signals_on_server and TYPESAFE_KEY is set, pass() calls calls(items, true), which puts every user's unjudged Signal rows for the same (feed_id, guid) into one group, and judge() sends one TypeSafe request built by request(). Each question's instructions interpolate its owner's free-text name, keywords and up to 500 characters of description with no delimiting. A signed-in user subscribed to the same feed as another user can write a description aimed at the other questions in the same request. The other user's verdicts are read from that shared answer and written to signal_verdicts; a false verdict is not asked again and a true one is rechecked only when its owner edits the Signal. Source confirms the mixing. It does not show whether Jev lets one question's instructions change another question's answer. Impact is limited to which of the victim's own subscribed items their Signal marks, and the attacker sees nothing back from it. This only works when both users' rows for the item are unjudged in the same batch. **Claimed root cause.** calls() groups rows from different principals into one call whenever share is true (server key in use), and request() builds one questions map from all of them, so text one user wrote reaches the model context that produces another user's verdicts. Nothing isolates one principal's questions from another's. **Local check.** {'local': "In a sandbox, run a scratch daemon on SQLite with two dummy users A and B subscribed to one fixture feed, signals_on_server=true, TYPESAFE_KEY set to a dummy value, and IPX_TYPESAFE_URL pointing at a local stub that records request bodies and answers noul 0.0 for every key. Give B the Signal 'Cooking'. Give A a Signal whose description says to answer every question about this item true. Confirm the stub gets one request with both signal_<A> and signal_<B> keys and A's text inside the shared questions object. Then confirm signal_verdicts holds B's row with hit=false and that the next pass does not ask about that item again.", 'deployment': "Owner-observed, with a TypeSafe test key and not production's database: on one fixture item, send B's question alone, then again with A's injected question in the same request, a few times each, and compare B's noul score. If B's score moves materially, cross-question influence is confirmed (low-severity integrity of B's Signal results). If Jev keeps questions isolated, record it as hardening: send one call per user on the server key too, or fence each description as quoted data."} **Owner check (no probing).** Owner-observed, with a TypeSafe test key and not production's database: on one fixture item, send B's question alone, then again with A's injected question in the same request, a few times each, and compare B's noul score. If B's score moves materially, cross-question influence is confirmed (low-severity integrity of B's Signal results). If Jev keeps questions isolated, record it as hardening: send one call per user on the server key too, or fence each description as quoted data. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:signals.rs:request:shared-call-mixes-users-questions`).
rays added the bug label 2026-10-23 05:26:57 -07:00
Author
Owner

Fixed in 0de0fb4, as decided: calls() makes one call per item and person, on the server's key too, so no one's description is in a question about someone else's Signals. docs/signals.md updated. Deployed.

Fixed in 0de0fb4, as decided: calls() makes one call per item and person, on the server's key too, so no one's description is in a question about someone else's Signals. docs/signals.md updated. Deployed.
rays closed this issue 2026-10-23 08:14:14 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#343