# PiCloud CMS PoC โ€” Security Audit Security findings from building & probing the CMS. Each is tagged **[PiCloud]** (the weakness originates in the platform / SDK) or **[CMS]** (our application code, i.e. a mistake the platform lets you make). Cross-referenced with [FINDINGS.md](FINDINGS.md). Severity: ๐Ÿ”ด critical ยท ๐ŸŸ  high ยท ๐ŸŸก medium ยท ๐Ÿ”ต low/info --- > **UPDATE 2026-07-18 (`fix/cms-poc-findings`): S-01/S-12 FIXED and re-verified.** > The user-route path now runs errors through the shared `scrub_runtime_detail` > helper: the client receives only `{"error":"script execution error (ref: > )"}` (HTTP 502) and the full detail is logged server-side under the same > correlation id. Live-verified with a `throw` carrying a secret marker โ€” no > marker, app-id, function name, or source line reached the client. The > highest-impact platform issue in this audit is closed. (S-02, the *no per-row > authz* design point, remains by design โ€” but its blast-radius amplifier is > gone.) ## S-01 ๐ŸŸ โ†’๐ŸŸข [PiCloud] Uncaught script errors return a 502 that leaks script internals โ€” FIXED **Finding:** Any uncaught Rhai error โ€” including an intentional `throw`, but also an unexpected SDK error (KV/docs failure, a `users::*` throw, a type error) โ€” is returned to the HTTP client as a **502** whose JSON body embeds: the **app UUID** (`app:2cced4ba-e20b-44c9-9080-d698cc2d4cca`), the **function names** in the call stack (`require_user`), and **line/column positions** of the script source. **Impact:** Information disclosure. An attacker who can trigger an error on any public route (malformed input, oversized payload, a value that makes a script throw) harvests the app's internal id and a map of its script structure. The app id is a capability-shaped identifier used across the admin API. **Attribution:** Platform. A script author cannot change the 502 shape; the only defense is to `try/catch` *every* fallible SDK call and every code path โ€” impractical and easy to miss. The platform should return an opaque 500/502 and log the detail server-side, not ship the stack to the client. **CMS mitigation:** the `auth::guard()` pattern never throws across the endpoint boundary, and endpoints wrap `users::create`/parse steps in `try/catch`. This is defense the *app* must remember to apply everywhere; coverage is only as good as author discipline. --- ## S-02 ๐Ÿ”ต [PiCloud] Public route = full app authority (documented footgun) **Finding:** A script bound to a public route runs with `principal: None` yet holds **full app authority** โ€” it can read/write every KV/docs/files collection and read every secret. There is no platform-level per-row or per-collection authorization; `authz::script_gate` returns Ok for an anonymous caller. The script body is the *only* access boundary. **Impact:** Every authorization decision in the CMS (is this user an admin? may they edit this post? may they read this draft?) is app code the author must get right. A single missing check exposes the whole app's data. See S-03 (IDOR) and the S3/S4 audit rows. **Attribution:** Platform design (documented in `docs/sdk-shape.md`). Noted here because it is the root cause that makes S-01/S-03-class bugs so high-impact: there is no backstop. **CMS mitigation:** the `auth` module is imported by every privileged endpoint; public endpoints are enumerated and each scoped to only published/approved rows (verified in later stages). _(Further rows added as later stages are probed: comment XSS, IDOR on docs ids, password-reset tokens, enumeration/rate-limits, email header injection, SSRF, secret exposure, SSE topic authz.)_ --- ## S-03 ๐ŸŸข [CMS] IDOR / RBAC enforced in-script โ€” verified **Finding (positive, with caveat):** Because the platform provides no per-row authz (S-02), the CMS enforces it in code. Probed: - author editing another author's post โ†’ **403** (`post_update` checks `cur.author_id == user.id` unless admin); - reader creating a post โ†’ **403** (`auth::guard(["admin","author"])`); - anonymous hitting an admin route โ†’ **401**; - public `GET /cms/posts/:slug` on a draft โ†’ **404** (status re-checked after slugโ†’id resolve, so guessing a draft's slug reveals nothing). **Caveat / attribution:** every one of these is app code that could have been omitted with no platform warning. The doc id is a UUID (not enumerable), which helps, but the security of the whole content model rests on each endpoint importing `auth` and re-checking ownership. This is the S-02 footgun realized: correct here, but only by discipline. A missing `guard()` on any single admin route would be a silent full bypass. --- ## S-04 ๐ŸŸข [CMS+PiCloud] Stored-XSS defense via interceptor โ€” verified, with a rendering contract **Finding:** Comment bodies + author names are HTML-escaped at write time by the `comment_sanitize` docs interceptor (see FINDINGS F-018). Probed `