Scope: workers/ (the room media cache, the edge cache, the click-id tag, the metrics dashboard, the email command queue, the vault picks store, the eBay deletion endpoint) and js/ (checkout and pricing, magic links and auth, the players, rooms, analytics, TikTok posting). Real defects only. No product code was changed.
| id | severity | file:line | defect | concrete failure scenario |
|---|---|---|---|---|
| WJ-01 | high | workers/premium-audio/src/worker.js:15 (enforced at :253) | A signed audio URL lives 3600 seconds, but 108 of the 1074 deployed sessions in content/session-registry.json run longer than an hour (longest 9172 seconds), and nothing on the session page re-signs. | A subscriber presses play on a 2 hour 33 minute sleep story. The browser fetches the MP3 in byte ranges as playback advances. The first range request issued after minute 60 gets 403 "Token expired", the audio element errors, and the story stops with the listener asleep and no retry path in _layouts/session.njk. |
| WJ-02 | medium | workers/premium-audio/src/worker.js:169 | The paywall_anomaly beacon for the period_end_passed and no_period_end denials is fired without await and without ctx.waitUntil (the fetch handler at :99 takes no ctx), and the 403 is returned on the next line. | A subscriber whose row says active but whose paid period has lapsed (the exact silent-fail vector the comment names) is refused. The runtime finishes the response and can cancel the pending POST, so the hourly reconciler never sees the anomaly and no Telegram alert fires. The sign_denied console line is the only trace. |
| WJ-03 | medium | workers/email-cmd/src/worker.js:343 (same test at :140) | The only authentication on the inbound instruction channel is a case-insensitive substring match of the envelope sender against ALLOWED_SENDER. No SPF, DKIM or DMARC result is read, and "includes" also accepts any address that merely contains the allowed one. | Anyone sends a message to the routed address with MAIL FROM set to the allowed address (or to another mailbox whose address contains it). The worker stores the body as a pending instruction and the local daemon that polls /instructions executes an attacker's text. Liveness of the cmd route was not checked in this review; if the route is retired the exposure is nil. |
| WJ-04 | medium | js/analytics.js:199 (backfill at :230, storage write at :211) | The header comment (:45 to :50) promises ephemeral mode with "zero enrichment" and "zero storage writes" when consent is absent, but the ipapi.co lookup, the sessionStorage write and the backfill_page_view_geo RPC all run regardless of consent; only the visitor-level geo update is gated. js/pricing.js:106 makes the same third-party call as a fallback. | A visitor declines cookies. Their IP address is still sent to ipapi.co, and city, region, latitude and longitude are written to their page_views row. The stated PECR position for ephemeral mode is not what the code does. |
| WJ-05 | low | workers/premium-audio/src/worker.js:611 (cap at :623) | MAX_RANGE_LENGTH only applies to open-ended ranges; an explicit "bytes=0-99999999999" or a plain GET with no Range header returns the whole object. The "8 MB cap per range request" the constant advertises does not exist. | Someone holding a valid signed URL fetches the full 80 MB story in one request as many times as they like inside the hour. The cost-amplification control the code claims to have is bypassed by omitting the header. |
| WJ-06 | low | workers/metrics/src/worker.js:128 | POST /ads/refresh, /board/refresh and /launchkit/refresh are state-changing and authenticated only by HTTP Basic credentials, with no Origin, Sec-Fetch-Site or token check. Browsers attach cached Basic credentials to a top-level cross-site form POST. | While the dashboard login is cached in the browser, a page elsewhere auto-submits a form to metrics.salus-rooms.com/ads/refresh. adsite-refresh and metrics-harvest dispatch on GitHub, re-pulling Google Ads, every time the page is loaded. |
| WJ-07 | low | workers/click-id/src/worker.js:59 | GET /cid sets the 90-day salus_gclid, salus_gbraid and salus_wbraid cookies for any request carrying a well-formed value, with no Sec-Fetch-Site or Referer check. | A third-party page embeds an image tag pointing at salus-rooms.com/cid?gclid=X. Every visitor to that page now carries click id X. When one of them later signs up, js/auth.js:200 writes X into signup_attribution and the offline conversion upload credits the wrong Google click. |
| WJ-08 | low | js/auth.js:405 (same rule at js/premium-gate.js:37) | The web client treats an active or trialing row with a NULL current_period_end as premium, while the worker (workers/premium-audio/src/worker.js:518 to :529) fails such a row closed unless plan_type is lifetime. The two rules disagree. | If get_my_active_subscription ever returns an entitling row with no paid period and no lifetime plan, the page removes the gate and adds is-premium, then /sign answers 403 and the session page shows the premium gate it just hid. Whether the RPC can return such a row was not verified here. |
| WJ-09 | low | js/auth.js:637 | The public SalusAuth API exports no refresh() method, yet _layouts/session.njk:499 guards its 401 retry on window.SalusAuth.refresh. The guard is always false, so the retry is dead code. | A /sign call answers 401 for a stale access token. The page never re-signs and falls straight to the premium gate for a paying subscriber. Mitigated in practice by the Supabase client's own token refresh, which is why this is low. |
| WJ-10 | low | js/room-video.js:76 (also :62 and :163) | embed_url, playback_url and poster_url are taken from the room_live_streams row and assigned straight to iframe src, video src and img src with no scheme check. | A javascript: value in embed_url runs in the salus-rooms.com origin for every room visitor. The row is admin-writable under RLS, so this needs a compromised admin login or a policy slip, which is why it is low. |
| WJ-11 | low | js/salus-room.js:21 (admin ids also at js/live-control.js:7) | An owner email address and two admin user ids are hard-coded in public client JavaScript served to every visitor. | Anyone reading the page source learns the owner's personal email and the admin account ids, which are useful for targeted phishing and for spotting the admin in the room roster. |
| WJ-12 | low | js/auth.js:218 | signin_audit rows are inserted with the anon key and the caller supplies email and user_id, so the audit log accepts unauthenticated writes naming any person. | Anyone with the public anon key posts sign-in rows for arbitrary emails. The auth matrix tracker that reads this table as truth shows green dots for sign-ins that never happened. |
| WJ-13 | low | workers/email-cmd/src/worker.js:353 (decode at :364 to :368) | The hand-rolled MIME parser matches "Content-Type: text/plain" case-sensitively, splits on the outermost boundary only, then applies base64 or quoted-printable decoding to whatever chunk matched, so a nested multipart/alternative part is decoded as a whole and atob throws. | An iPhone Mail reply (multipart/mixed wrapping multipart/alternative, base64 text part) reaches handleEmail. atob throws inside the email handler, the exception is uncaught, and the instruction is lost. |
| WJ-14 | low | workers/public-assets/src/worker.js:146 (same at workers/premium-audio/src/worker.js:239 and workers/metrics/src/worker.js:172) | decodeURIComponent is called on the raw path with no try/catch, so a malformed percent sequence throws URIError. public-assets has no outer handler, so the request ends as a worker exception. | A scanner or a broken link requests media.salus-rooms.com/images/%E0%A4%A. The worker throws, Cloudflare answers with its 1101 error page instead of 404, and the error lands in observability as an exception. |
Every source file under workers/ and js/ was read in full by hand: 18 files in workers/ (click-id, ebay-account-deletion, email-cmd, metrics, premium-audio, public-assets and vault-picks: each worker.js and wrangler.toml, plus the eBay WAF rule, the premium-audio mutation harness and its two test suites) and 18 files in js/ (ambient-player, analytics, auth, chat-widget, feeling-search, listen-promo, live-control, main, premium-gate, preview-manifest, pricing, room-video, salus-room, session-loader, supabase-config, tiktok-media, tiktok-share, tiktok-upload). The six JSON data files in js/, the .wrangler cache files and the sample MP3 were skipped as data, not code. To confirm failure scenarios rather than guess, five files outside the scope were consulted read-only: _layouts/session.njk (the /sign caller), _includes/head.njk (the /cid caller), _includes/scripts.njk, _data/sessionsList.js and content/session-registry.json (session durations). No live service was queried, no build or deploy was run, and no product file was edited. Findings are limited to correctness, security, payment-path, data-loss and race defects; style was ignored.