flower
/
All briefs
idea draft review_synthesis vodmanager
epic · VOD Manager — post-review roadmap (ambassador launch...

Livewire security review — findings & hardening (SSRF is the one live pre-launch fix)

Dispatch

canonical · plan

Spec

markdown

hand-off · dispatch

Dispatch

Auto-dispatch

when it reaches planned

Design-loop

design pass before build

Direct dispatch — no refine required. The packet tells the agent to ask questions only if the request is blocked by ambiguity.

kind

No dispatch requests yet — dispatch above to generate a copy-paste packet.

provenance · append-only

Trace

live
or paste a screenshot uploading…
  1. refinement 11h ago

    PROGRESS (commit e5b9415, held main): H-1 SSRF FIXED (new App\Rules\PublicEndpointUrl on storage endpoint_url, wired into StorageProfileForm rules() + testConnection(); tests in StorageEndpointSsrfTest). M-1 FIXED (PurgeAccountBucketsJob implements ShouldBeEncrypted). REMAINING in this brief: M-2 (feature:* middleware not persistent across Livewire updates → mirror Features::allows() in mount() + each action for StreamAnalytics/StreamHighlights/WrappedYear+Month/highlights VodList); L-1 (2FA recovery codes baked into DOM); all hardening (rate limits, per-element validation, createClip dup-dispatch race, Nightwatch redact list); and systemic (ownership-scope helper, #[Locked] arch-test for scalar *_id props, egress allowlist for the replay-proxy too, allow_redirects=false + generic error-oracle message on the storage validator as the SSRF follow-up hardening).

    agent · claude-code
  2. parent set 11h ago

    Grouped under epic #272.

    agent · claude-code
  3. note added 2d ago

    Code-grounded security review of all 66 Livewire components (6 finder agents → adversarial Opus verify against Livewire v4.2.4 source). Posture is STRONG: ownership re-derived from fresh Auth::id()-scoped queries at every mutation, Rule::in/allowlists on mass-assignment, all Blade output escaped, creds encrypted at rest. NOTE: these findings are about the EXISTING production app (storage profiles, account deletion, feature-gated pages) — not the held ambassador feature. == CONFIRMED (verified) == H-1 · SSRF via unrestricted endpoint_url — HIGH — the one worth fixing soon (live in prod). - App\Livewire\Storage\StorageProfileForm::save()/testConnection() → StorageProfileValidator::checkCredentials() → StorageProfileFilesystemFactory::client(). Route storage-profiles (auth,verified) — ANY logged-in user, no admin. - endpoint_url validated only as url|max:255 (no scheme/host/private-IP restriction). Flows into new S3Client(['endpoint'=>...]) and checkCredentials() fires real outbound headBucket/put/delete from the prod box. Attacker sets provider=other_s3, endpoint_url=http://169.254.169.254/... (cloud metadata) or any RFC1918 host and clicks "Test connection". Unrated, scriptable. - Amplified: humanizeError() returns $exception->getMessage() verbatim for non-S3 exceptions → Flux toast, turning it into an internal port-scan/service-probe oracle. - FIX: validation rule that resolves endpoint_url host and rejects loopback/link-local(169.254.0.0/16)/RFC1918/ULA/reserved BEFORE building any client, in both rules() and testConnection(); set allow_redirects=false on the SDK Guzzle handler; collapse humanizeError() non-S3 branch to a generic message. M-1 · Plaintext S3 creds in Redis job payload — MEDIUM. - App\Livewire\Settings\DeleteUserForm::deleteUser() → PurgeAccountBucketsJob implements ShouldQueue only (NOT ShouldBeEncrypted). Its public array $storage_profile_snapshots holds access_key_id/secret_access_key in cleartext, serialized into shared Redis db 7 (+ any RDB/AOF/MONITOR) until drained. Bypasses the encrypted casts used everywhere else. - FIX (one line): implements ShouldQueue, ShouldBeEncrypted. M-2 · Feature-tier gate bypassable on Livewire update requests — MEDIUM. - feature:* → App\Http\Middleware\EnsureFeatureEnabled is CUSTOM middleware, not in Livewire's persistent-middleware list and not registered via Livewire::addPersistentMiddleware(). So it runs only on the initial full-page GET; subsequent actions hit /livewire/update and skip it. StreamAnalytics, StreamHighlights, WrappedYear/Month, and the highlights VodList re-check ownership but never re-check Features::allows(). A downgraded/revoked user with the tab open keeps invoking createClip/rerunDetection/etc. Bounded to their OWN streams (ownership always enforced) → tier bypass, not cross-tenant. - FIX: abort_unless(Features::allows(Auth::user(),'<feature>'),404) in mount() AND at the top of each mutating action (or a shared booted hook), for all the above. L-1 · 2FA recovery codes always in the DOM — LOW. - resources/views/livewire/settings/two-factor/recovery-codes.blade.php:7 bakes plaintext codes into Alpine x-data on render; the reveal toggle is cosmetic (CSS only). FIX: load codes on-demand behind the reveal action (post password-confirm), not in initial x-data. == HARDENING (defense-in-depth) == - Rate-limit: BackfillButton::enqueue + VodList::refreshFromTwitch (also exclude in-flight download_requests, which lack a twitch_vod_id unique constraint); StreamShow::recheckForVod (short-circuit when IN_PROGRESS); TwitchAccountSettings::repairEventSub; Security::confirmPassword + DeleteUserForm current_password (password-guessing oracle). - Per-element server validation: StreamTagsEditor::$tags (cap each element <255 to avoid QueryException); ChannelsIndex::$add_backfill_limit (nullable|integer|min:0|max:100). - Duplicate-dispatch TOCTOU: StreamHighlights::createClip / StreamShow::createClip — generated_clip_vod_id-null guard races; use ShouldBeUnique keyed on highlight_id or a lockForUpdate claim. - Add StorageProfileForm access_key_id/secret_access_key + OAuth secret fields to NIGHTWATCH_REDACT_PAYLOAD_FIELDS now (capture is off today, but future-proof). == DISMISSED on verification (don't chase) == - UserDetail::$user / VodDetail::$vod / AmbassadorDetail::$ambassador "model-swap IDOR" — NOT exploitable. Livewire v4 checksum-protects model meta with HMAC keyed on APP_KEY; ModelSynth::set() throws. Missing #[Locked] on MODEL-typed props is cosmetic. - VodList retry* "zero authz" — can:admin = Illuminate Authorize IS persistent middleware; re-runs every update. == SYSTEMIC == 1. Custom route middleware does NOT persist across Livewire updates (root cause of M-2). Only Livewire's built-ins (Authenticate/Authorize/SubstituteBindings) re-run on /livewire/update. feature:* AND verified do not. Any authz as custom middleware must be mirrored in-component or registered via Livewire::addPersistentMiddleware(). Audit every non-default ->middleware() on a Route::livewire line. 2. #[Locked] discipline: model-typed props already protected; the REAL risk is scalar *_id/array public props (client-writable via IntSynth/ArraySynth). Add an arch-test asserting every scalar *_id public prop in app/Livewire is #[Locked] or ownership-revalidated. Stop adding #[Locked] to model props as a "security control." 3. Extract ownership-scope helpers (TwitchAccount::ownedBy / Stream::scopeOwnedBy + an authorizeOwner trait) — the pattern is right but hand-repeated in ~10 components. 4. mount() checks + every mutating action re-derives (StreamHighlights is the good template). 5. Egress allowlist: one reusable "public host only" rule + hardened HTTP/S3 factory (no redirects, post-DNS host re-validation) routing ALL user-influenced endpoints (endpoint_url + the replay-proxy). 6. Secrets-at-rest in transient carriers (Redis jobs, telemetry): ShouldBeEncrypted on any job carrying credential snapshots; keep Nightwatch redact list a superset of every credential field. Related: deploy-readiness #146. Suggest fixing H-1 + M-1 near-term regardless of the ambassador hold (both are quick and live in prod).

    agent · claude-code
  4. participant joined 2d ago
    system · claude-code

epic · dependencies

Relationships

depends on

No dependencies — dispatchable once planned.

agents · waves

Participants

  • claude-code participant · active

trace · graph

Links

No links yet — they accrue as agents work the brief.

scope

Projects

  • vodmanager · primary

dogfood · read-only

Agent’s-eye view

The literal recall_brief payload an agent gets — same service path as the MCP tool.