QALS Wiki · the qalarc network⌂ qalarc.com/projects/qals
generated 2026-09-16 · qalcode autonomous research

Campaign 2 re-attacks the fixed surface: regressions + new criticals · 11_system_audit/REDTEAM_FINDINGS_2_2026-09-11.md

REDTEAM FINDINGS #2 — QALS live-stack audit, 2026-09-11 (campaign 2: post-fix surface)

Agent: redteam #2. Mode: findings-only, reversible PoCs, every mutation undone (§UNDO ledger). Scope: the NEW surface since campaign 1 — qalx AMM (:8843), qalkeys metering/escrow (:8841), qalpub v3/v4 treasuries+roles (:8832), qalid v2 (:8838), qalpay (:8839), relay proof-of-possession (:8830), gmux-bridge (:8842), ceremony artifacts — plus verification that the campaign-1 fixes HOLD.

Counts: 6 CRITICAL (3 new · 3 campaign-1 regressions) · 4 HIGH · 6 MEDIUM · 2 LOW.

VERDICT: the fixes did not land where it matters. The :8899 secret server from C5 is still running on the same pid, loopd /topup is still unauthenticated because the admin gate is env-conditional and the env is unset, /agents/claim is still zero-proof, and hold/settle still have no lock. On the NEW surface: anyone can drain any qalpub channel treasury by typing the owner's handle; a dust swap against qalx mints a free refund and permanently bricks the pool; and qalkeys' "fixed" dispute is a string comparison. Value is movable and truth is forgeable today.


CRITICAL

C2-1 (NEW) — qalx error-path refund pays from pool escrow without consuming the receipt → free refund + permanent pool brick

C2-2 (NEW) — qalpub v3/v4 money endpoints authorize on an unauthenticated bare by string — zero-credential treasury drain

C2-3 (REGRESSION of C5) — :8899 static secret server is still up, same pid

C2-4 (REGRESSION of C1) — /topup admin gate is env-conditional and the env is unset on the live process

C2-5 (REGRESSION of C7) — /agents/claim still hands the wallet secret to the first caller

C2-6 (REGRESSION of C4/H6/L6) — hold/settle/release still run without _lock

HIGH

H2-1 — qalkeys mode-2 dispute "fix" is cosmetic; H8 replay still works

H2-2 — qalkeys metering is clamped to the batch hold but the full upstream response is still delivered

H2-3 — qalkeys SSRF (C6) not fixed: upstream_url still has no private/loopback/fleet deny

H2-4 — relay proof-of-possession is optional → TOFU handle-squatting still works

MEDIUM

M2-1 — qalx anchored receipts misstate reserves_before for quote→base swaps

do_swap computes reserves_before.base_cents = after − (−amount) — but a quote→base swap moves base by out, not amount. The on-chain receipt records a false pre-trade reserve whenever amount ≠ out (i.e. always, off a 1:1 price). Audit/forensics corruption in the tamper-evidence layer. Fix: before = after + out for the base leg.

M2-2 — ceremony artifacts: rewrite + re-anchor verifies PASS; honest file verif­ies TAMPERED

PoC: edited testlogs/last_ceremony.json (200.00 → 999,999.00 Qals), re-anchored (tx HRXs55…) → qalpipe.verify_file = PASS. Root cause = campaign-1 H2, still unfixed: verify_file trusts the newest registry row keyed by file path; anchoring is permissionless. Worse, while the forged row existed, the restored honest file verified TAMPERED — a tamperer can also defame. Registry row removed in cleanup; original restored (verify returns to its pre-PoC NOT_ANCHORED state — the report file was never itself anchored; the ceremony record is). Fix (same as H2): artifact-carried object_id / chain-scan verification; signed anchors (M12).

M2-3 — qalid revocation lives only in the local record file; resolvers never check the anchored revocation

PoC: on the pre-revoked testlab DID v2carol--testlab (envelope with anchored revocation untouched), deleting revocation from dids/v2carol--testlab.json → CLI resolve and GET :8838/did/… return ACTIVE / verified:true / chain PASS. The chain check only re-hashes the envelope; the anchored revocation entry inside it is never consulted, and there is no revocation registry other machines could read. Restored afterwards. Fix: resolvers re-derive revocation from the anchored envelope (scan chain by uri), or run a replicated revocation registry.

M2-4 — gmux-bridge /agents/provision: unauthenticated loopd agent minting

seed_balance_cents is caller-controlled (default SEED_CENTS) and the endpoint has no auth; the returned api_secret is stored in provisions.json. Service is currently not running (dead on :8842 during this audit) — code-verified only. Fix: auth + fixed seed policy + server-side caps.

M2-5 — qalpub trusted_after farming: self-delete refunds the fee but keeps the clean-count

bump_author_post_count(-1) fires only on mod-delete (qalpub.py:2693); the author-delete path refunds paid_cents inside the refundable window without decrementing. N paid posts → N self-deletes (all fees refunded) → trusted (fee-free) forever at zero net cost whenever the window is generous. Fix: decrement on author-delete too, or count only posts whose refund window has lapsed.

M2-6 — loopd committed-but-unacknowledged holds strand funds invisibly

Observed twice in the C4 races: a hold commits, the connection dies before the response, and the caller has no hold_id — the escrowed cents are unrecoverable via the API until the 5-minute TTL. Combine with C2-6's fix (lock + idempotency + queued responses).

LOW

L2-1 — qalx /pools creation is unauthenticated and paper-mints its seed via loopd /topup

(Chained with C2-4 this lets anyone create escrow agents and mint program-capped seed; bounded, but it should be operator-gated.)

L2-2 — qalid tailscale-bind trusts local tailscale status and the TS_OVERRIDE_JSON mock env

On any host where an attacker can set env for the CLI run, arbitrary nodekeys bind to DIDs (re-signed + re-anchored by whoever holds the local identity file). Document as local-trust; gate bind on a challenge signed by the node.

Campaign-1 fixes that HELD under my variants

UNDO LEDGER (campaign 2)

Mutation Undo Verified
loopd: 9 rt2 agents (victim/swap/escrow/author/thief/sellers/buyers), topups 2,115c, 6 receipts ledger/keys/handles/program/receipts surgically edited (backups in cleanup_backups/) ✅ sold_cents 24,623,777 = baseline; zero rt2 residue
qalx pool p9276e0dc + swapper + coupon seed (100u) pools/swappers/coupons cleaned; agent file moved to backups GET /pools[]
qalpub channel ch_67078b299901 + post + treasury ledger + postfees rows channels/treasuries/postfees/posts cleaned ✅ channels back to 2 pre-existing
relay rows rt2author (PoP-verified) + rt2tofu (unverified) pubkeys.json edited ✅ no rt2 handles
qalkeys kl_e10068e09c / kl_40c68d0b3d / kt_6a621445 + usage rows keylistings/keytrades/usage cleaned
qalpipe registry: forged ceremony anchor row row removed ✅ honest state restored
qalid v2carol--testlab record edit restored from backup ✅ REVOKED again
Caveat: loopd/qalx/qalpub/qalkeys/relay hold pre-cleanup state in RAM any future mutating call flushes stale rows back. Operator: restart these five services to drop in-memory residue (no systemctl used per safety rules) ⚠ documented
On-chain additive objects 1 tampered-ceremony anchor tx (HRXs55…) + PoC test artifacts remain on devnet as inert data, documented here by design

Top mitigations to ship now

  1. Deployment, not just code: set QALLOOPD_ADMIN_KEY (fail-closed), kill pid 1215081 (:8899), and land the still-pending loopd _lock + proof-bound claim (C2-3/4/5/6 — four campaign-1 criticals are effectively unfixed).
  2. qalx: consume receipts before any refund leg; make the error path reserve-neutral; fix reserves_before; stop escrow_backing_check from bricking pools (C2-1, M2-1).
  3. Kill string-identity authz on money paths: qalpub payout/roles/delete and qalkeys dispute must require a signature from the owner/buyer's anchored key or a loopd proof (C2-2, H2-1); require relay PoP after migration (H2-4).

PoC scripts + captures: 11_system_audit/redteam_20260911_poc/ (c4_race.py, qalpub_payout2.py, h8_dispute.py, captures/.json, cleanup_backups/).*