Security Transparency Report

Security Transparency Report

At Streamd, we believe in radical transparency when it comes to security. This report documents all security findings from our audits, how they were resolved, and what we're doing to keep your data safe.

Last updated: April 15, 2026 · Next audit scheduled: Q3 2026

Executive Summary

39/39
Issues Resolved
< 24 hours
Avg Resolution Time
1
Third-Party Audits
39
Total Findings
39
Resolved
1
External Audits
< 24 hours
Avg Resolution

Findings by Severity

Audit History

April 2026 Security Audit

Completed
39
Total
7
Critical
17
High
15
Medium
0
Low
39
Resolved

Security Findings & Resolutions

30 of 30 findings

Unauthorized Co-Parent Assignment Without Consent

STR-005
CriticalApril 14, 2026·Authorization
Description

Any authenticated user could forcibly assign any other user as a co-parent on their family, granting the victim full read/write access to child account data without approval.

Resolution

Replaced immediate account linking with an invite/accept flow. Invites expire after 48 hours and require explicit acceptance by the invitee.

Reference:CWE-862

Unauthenticated Stream Metadata Tampering and Identity Spoofing

STR-006
CriticalApril 14, 2026·Authentication
Description

Six signaling actions required no authentication. Attackers could spoof stream identity, tamper with metadata, and disrupt streaming without auth.

Resolution

All signaling actions now require valid session. Stream ownership verified before any write operations.

Reference:CWE-306

Race Condition in Parental-Control Unlock Activation

STR-008
HighApril 14, 2026·Concurrency
Description

Non-atomic read-check-write pattern allowed concurrent requests to activate multiple paid features for the cost of one.

Resolution

Added per-child distributed lock using Redis SET NX. All conditions re-verified after lock acquisition with automatic lock release.

Reference:CWE-367

Race Condition in Parental-Control Task Completion

STR-009
MediumApril 14, 2026·Concurrency
Description

Check-then-use pattern allowed concurrent completion requests to award duplicate points for a single chore.

Resolution

Added per-task distributed lock. Task status re-checked after lock acquisition to ensure idempotency.

Reference:CWE-367

Race Condition in Follow Workflow

STR-010
MediumApril 14, 2026·Concurrency
Description

Non-atomic relationship creation allowed concurrent follow requests to corrupt follower/following counters.

Resolution

Replaced existence check with Redis SET NX for atomic relationship creation. Added per-relationship lock for unfollow operations.

Reference:CWE-362

IP Header Spoofing in Parental Control Device Tracking

STR-011
MediumApril 14, 2026·Spoofing
Description

Device fingerprinting functions directly trusted X-Forwarded-For and X-Real-IP headers without validation. Attackers could spoof IP addresses to poison parental control device mappings, causing legitimate users from the same spoofed IP to be incorrectly flagged as using a child's locked device.

Resolution

Modified getDeviceId() and getIPAddresses() to prioritize runtime-provided request.ip (set by platform/CDN, unspoofable). Client headers are now only used as fallback in development mode (NODE_ENV !== 'production').

Reference:CWE-290

CSRF via State-Changing GET Endpoints

STR-012
MediumApril 14, 2026·Cross-Site Request Forgery
Description

GET endpoints for messages/fetch and notifications/followers performed state mutations (marking read, clearing notifications). SameSite=Lax cookies allowed cross-site triggers.

Resolution

Separated read from state-changing operations. GET endpoints are now read-only. Added POST/DELETE endpoints for mutations.

Reference:CWE-352

Parental Control Device Lock Bypass

STR-014
HighApril 14, 2026·Authorization
Description

The child account login flow created device locks on first login but failed to enforce them on subsequent logins. After initial device registration, children could log in from any device without restriction, completely bypassing parental device controls.

Resolution

Added device lock enforcement to the child account login flow. After first login, all subsequent logins verify device fingerprint, IPv4, IPv6 prefix, or MAC address matches the locked device. Login is rejected if no lock type matches, requiring parent approval for new devices.

Reference:CWE-862

Race Condition During Signup - Duplicate Account Creation

STR-018
HighApril 14, 2026·Concurrency
Description

Non-atomic check-then-set pattern for username/email uniqueness allowed concurrent requests to create accounts with duplicate credentials.

Resolution

Replaced two-step check+set with atomic Redis SET NX. Email and username claims now atomic with rollback on partial failure.

Reference:CWE-362

Unauthenticated Mux Webhook - VOD Metadata Tampering

STR-019
HighApril 14, 2026·Authentication
Description

VOD webhook endpoint accepted video.asset.ready events without signature verification, allowing attackers to tamper with VOD metadata and trigger unauthorized deletions.

Resolution

Implemented Mux webhook signature verification using mux-signature header with constant-time comparison.

Reference:CWE-306

Replayable Chore Completion - Unlimited Point Inflation

STR-020
HighApril 14, 2026·Logic Error
Description

Task completion lacked idempotency checks. Same request could be replayed indefinitely to inflate child point balance.

Resolution

Added guard that throws if task.completed === true. Concurrent replay requests now blocked.

Reference:CWE-841

Parental Device Lock Cleanup - Full Redis Keyspace Exposure

STR-021
HighApril 14, 2026·Information Disclosure
Description

Cleanup endpoints accessible to any parent. Used redis.keys('*') and wildcard patterns returning full parsed JSON of every matching record including unrelated family data.

Resolution

Replaced unrestricted scans with getManageableChildUserIdsForUser() returning only authorized child IDs. Removed rawData field from responses.

Reference:CWE-200

Unauthorized Message Request Handling

STR-022
HighApril 14, 2026·Authorization
Description

Respond endpoint allowed deletion or acceptance of message requests not addressed to the authenticated user.

Resolution

Added recipient verification ensuring only the intended recipient can respond to message requests.

Reference:CWE-862

Child Messaging Restriction Bypass

STR-023
MediumApril 14, 2026·Authorization
Description

Child accounts could bypass 'Send Messages' parental restriction and message unrelated adults.

Resolution

Enforced permission checks for child accounts. Messages now blocked unless recipient is parent, co-parent, sibling, or staff member.

Reference:CWE-863

Cross-Family Child Point Balance Access

STR-024
MediumApril 14, 2026·Information Disclosure
Description

Any authenticated user could read another family's child point balance through the parental control endpoints.

Resolution

Added authorization checks ensuring only parents/co-parents of the specific child can access point balance data.

Reference:CWE-200

TURN Relay Credentials Exposed to Unauthenticated Requests

STR-025
MediumApril 14, 2026·Information Disclosure
Description

TURN server credentials endpoint accessible without authentication, exposing ICE server configuration.

Resolution

Added session authentication requirement for TURN credentials endpoint.

Reference:CWE-306

Sessions Not Invalidated After Credential Change

STR-026
HighApril 15, 2026·Authentication
Description

Changing a password or security code (set, change, or remove) did not invalidate existing sessions. An attacker who had previously stolen or obtained a session cookie retained full authenticated access indefinitely after the victim rotated their credentials. The session cookie remained valid for its full 30-day TTL regardless of credential changes.

Resolution

Added invalidateOtherSessions() to lib/auth/auth.ts. On login, each new session ID is tracked in a per-user Redis set (user:{id}:sessions). After any credential mutation (password change, security code set/change/remove), all sessions for the user except the one performing the change are immediately deleted from Redis. The current session is preserved so the victim is not logged out.

Reference:CWE-613

Stale Co-Parent Authorization After Family Relinking

STR-027
HighApril 15, 2026·Authorization
Description

When a co-parent accepted an invite from a second family without first being removed from the first, addCoParent updated the co-parent's parentUserId field but did not remove them from the old family's parent:{oldId}:coparents Redis set. getAllParentsForChild() reads that set to decide who may access a child's data, so the co-parent retained full authorization on the original family's children indefinitely — long after they appeared to have switched families in the UI.

Resolution

Added an unlinking step to addCoParent: before linking to a new family, the function now checks whether the co-parent's accountType is already 'co-parent' with a different parentUserId, and if so atomically removes them from the old family's coparents set, deletes the old coparent link key, clears the old children from the co-parent's children set, and removes them from child moderator lists. The guard executes before the new link is written.

Reference:CWE-613

Rate-Limit Bypass via Spoofed X-Forwarded-For Header in User Search

STR-028
MediumApril 15, 2026·Spoofing
Description

The user search rate-limit key was derived exclusively from client-supplied IP headers. An authenticated attacker could rotate spoofed IP values to create fresh rate-limit buckets and bypass the per-IP cap, enabling unlimited user enumeration with only a single valid session.

Resolution

Replaced the client-header-only IP resolution with the same pattern used elsewhere: the platform-provided runtime IP is the authoritative source in production, with client headers only used as development fallback.

Reference:CWE-290

Static TURN Relay Credentials Exposed to Authenticated Users

STR-029
MediumApril 15, 2026·Information Disclosure
Description

The TURN relay configuration endpoint returned the raw long-lived master credentials directly to any authenticated user. These credentials never expire. Any logged-in user could extract them and use the relay service independently of the application, incurring bandwidth costs and enabling abuse.

Resolution

Replaced static credential passthrough with a server-side call to the relay provider's temporary credential API. Master credentials now stay server-side only. Clients receive short-lived time-bound tokens generated per-request. If the provider API is unreachable, the endpoint falls back to STUN-only rather than exposing static secrets.

Reference:CWE-522

Parental Control view_profiles Permission Not Enforced on Profile API

STR-036
MediumApril 15, 2026·Authorization
Description

The view_profiles parental permission was evaluated exclusively by an advisory client-callable check endpoint. The actual profile data endpoint performed no parental permission check. A child account with view_profiles disabled by their parent could bypass the restriction entirely by calling the profile data endpoint directly, receiving full profile data including bio, avatar, badges, suspension status, and live stream metadata for any user.

Resolution

Added server-side view_profiles enforcement to the profile data endpoint. After session resolution, child accounts viewing another user's profile are checked against hasPermission() for view_profiles, with a fallback to childHasViewProfileExemption() to allow viewing the child's own profile and sibling profiles. Requests failing both checks receive 403.

Reference:CWE-602

Viewer Lifecycle Authorization Bypass: Ban Evasion, Identity Spoofing, and Unauthenticated Viewer Eviction

STR-035
HighApril 15, 2026·Authorization
Description

Five viewer lifecycle signaling actions had authorization defects. (1) Ban bypass: the ban check was gated on whether a userId was present in the request body, so a banned user could omit the field entirely and rejoin the stream as an anonymous viewer. (2) Identity spoofing: the viewer registration and heartbeat actions accepted a body userId without validating it against the authenticated session, allowing any caller to map their viewer slot to another user's identity. (3) Viewer enumeration: the broadcaster viewer polling action required no authentication, allowing any caller to enumerate all viewer slots and their associated user IDs for any stream. (4) Profile disclosure: the viewer profile resolution action required no authentication, returning usernames and avatars for all active viewers. (5) Viewer eviction: the disconnect action required no authentication and no ownership check, allowing any caller to remove any other viewer from any stream.

Resolution

All five handlers now derive viewer identity exclusively from the authenticated session. The viewer registration and heartbeat actions ignore any body-supplied user identifier. The ban check runs against the session identity unconditionally. The broadcaster viewer polling action requires auth and verifies the caller is the stream owner. The viewer profile resolution action requires any authenticated session. The disconnect action requires auth and verifies the session user matches the stored viewer mapping before allowing removal.

Reference:CWE-284

Unauthenticated Watch-Party State Overwrite and Follower Alert Injection

STR-034
MediumApril 15, 2026·Authorization
Description

Three signaling actions were reachable without authentication and without any ownership check. (1) Watch-party sync: any unauthenticated caller could overwrite the co-watch playback state for any stream, desyncing all viewers in an active session. (2) Follower alert injection: any unauthenticated caller could inject fake follower alert entries into any live stream's alert queue, causing fraudulent notifications on the broadcaster's overlay. (3) Alert queue read: any unauthenticated caller could read the pending alert queue for any stream, disclosing follower usernames and timing metadata.

Resolution

All three handlers now require an authenticated session (401 if absent). The watch-party sync and alert read actions additionally verify that the authenticated user is the stream's broadcaster (403 otherwise). The follower alert action derives the display name from the authenticated session rather than the request body, preventing spoofing by authenticated users.

Reference:CWE-306

Username Enumeration via Login Response Differential

STR-033
MediumApril 15, 2026·Information Disclosure
Description

The login endpoint was exploitable for username enumeration through two independent differentials. (1) Message differential: a valid username with a wrong password would eventually produce a distinct lockout error message after repeated failed attempts, while a nonexistent username always returned the same generic error regardless of attempt count. An attacker could distinguish valid from nonexistent accounts by observing when the error message changed. (2) Timing differential: nonexistent usernames returned immediately after a database lookup miss, while valid usernames with wrong passwords ran a full password hash comparison first. The latency gap was reliably detectable with a single request.

Resolution

Two fixes applied to the login handler. For timing: a dummy password hash comparison is now performed for every nonexistent-username rejection before returning, equalising response time with wrong-password rejections. For message: the lockout error is now caught and returned as the same generic error string as all other failed attempts, making all failed login responses textually identical regardless of whether the account exists or is locked.

Reference:CWE-204

Demoted Staff Self-Restoration via Username Change

STR-032
HighApril 15, 2026·Authorization
Description

The username-change path in the profile update flow contained badge-preservation logic that ran whenever a user changed their username. The logic checked for the mere existence of an admin account record in the data store and would reactivate an inactive staff badge or create a missing one if any record was found. Because the demotion process sets isActive=false on the account record but does not delete it, a recently demoted user could trigger the badge restoration by simply submitting a username change. This restored the staff badge and all staff-protected capabilities (anonymous mode, stream moderation immunity, etc.).

Resolution

Fixed the badge-preservation block in the profile update flow to gate on adminAccount.isActive === true, not merely on the existence of the account record. Both the reactivation branch (inactive badge) and the creation branch (missing badge) are now guarded by this check, so a demoted user with an inactive admin account record cannot trigger either path.

Reference:CWE-269

Chat Signaling Identity Spoofing and Broadcaster Impersonation

STR-031
HighApril 15, 2026·Authentication
Description

Four chat signaling actions accepted identity fields directly from the request body without verifying them against the authenticated session. (1) The send-message action trusted client-supplied user ID, username, and broadcaster flag, allowing any caller to post messages as any other user or claim broadcaster status. (2) The message retrieval action used a body-supplied user ID to decide whether to expose admin-hidden messages, so any caller could supply the broadcaster's ID and see moderation-hidden content. (3) The delete action used a body-supplied user ID for permission and attribution checks, allowing impersonation of the broadcaster or moderator to delete messages. (4) The allow action used a body-supplied user ID to check broadcaster status, so any user could restore admin-hidden messages by supplying the broadcaster's ID.

Resolution

All four handlers now derive identity exclusively from the server-side session. All body-supplied identity fields are ignored for authorization. The send, delete, and allow actions return 401 if no valid session is present. The message retrieval action remains publicly readable but hidden-message visibility is now gated on the authenticated session matching the broadcaster's stored identity.

Reference:CWE-290

Child Messaging Family-Allowlist Bypass via Non-Existent Redis Keys

STR-039
MediumApril 15, 2026·Authorization
Description

Three endpoints that enforce child messaging restrictions built the family allowlist from Redis keys that were never populated by any part of the application: parents:{userId}, coparents:{userId}, and siblings:{userId}. These keys always returned empty sets, so the family-member exception that is supposed to permit restricted children to message their own parents, co-parents, and siblings silently failed. The same defect existed independently in the send-message route, the fetch-messages route, and the check-message-permission advisory route. The advisory endpoint additionally called getCoParents() with the child's own user ID instead of the primary parent's ID, and compared the returned strings against a .id property that does not exist on strings, ensuring the co-parent check always returned false. The net result was that children with send_messages disabled could not message their own family — a stricter restriction than intended — but the converse (children bypassing restrictions to message non-family) did not occur.

Resolution

Replaced all three incorrect family-lookup blocks with a single call to the existing isFamilyCircleMemberOfChild() library function, which correctly resolves the primary parent, co-parents, and siblings from the canonical Redis key patterns used by the rest of the parental-control system. The unused getSiblings() helper in the advisory route and its stale imports were removed.

Reference:CWE-284

Child Account Parental-Control Bypass via Subordinate Account Creation

STR-038
CriticalApril 15, 2026·Authorization
Description

The child account creation endpoint did not verify that the authenticated caller was a parent or co-parent account. Any child account could call the child account creation endpoint and successfully invoke createChildAccount() with their own user ID as the parentUserId. Because isParentOrCoParent() determines authority by reading the parent:{id}:children Redis set — which createChildAccount() populates unconditionally — the child became the recognized parent of the new account. The child could then call the permissions endpoints, pass the isParentOrCoParent authorization check, and grant the subordinate account any capability (send_messages, streaming, etc.) regardless of the restrictions placed on the child itself. The child could also use the subordinate account to access features they were personally blocked from.

Resolution

Added an account-type guard as the first check in the child account creation POST handler: if the caller's accountType is 'child' the request is rejected with 403 before any account creation logic runs. Added the same guard to both the GET and POST handlers of the permissions endpoint as defence-in-depth, so child accounts are blocked from reading or writing child permissions even if a pre-existing unauthorized parent relationship exists in Redis.

Reference:CWE-269

Stream Moderator Privilege Escalation via Self-Assignment

STR-037
HighApril 15, 2026·Authorization
Description

The assign-stream-moderator signaling action checked that the caller was either the stream broadcaster or held the assign_moderators permission, but did not prevent a moderator from targeting their own user ID. A moderator who had been granted only assign_moderators (without ban_user, timeout_user, block_chat, or delete_messages) could call the endpoint with targetUserId set to their own ID and request the full permission set, unconditionally overwriting their Redis moderator record with all five permissions. The same gap allowed any delegating moderator to grant permissions they did not themselves hold — for example, a moderator with only assign_moderators could elevate a third party to full moderator status with ban and timeout capabilities the broadcaster never intended to grant.

Resolution

Added two guards to the assign-stream-moderator handler. First, self-targeting is now explicitly rejected: if targetUserId equals the authenticated caller's ID the request is refused as an invalid target. Second, non-broadcaster callers are now restricted to granting only permissions they currently hold — the requested permissions array is filtered against the caller's own permission set before the moderator record is written. Broadcasters remain unrestricted and may grant any valid permission to any eligible user.

Reference:CWE-269

Unauthenticated User Presence and Last-Login Disclosure

STR-030
MediumApril 15, 2026·Information Disclosure
Description

The user online-status endpoint required no authentication. Any unauthenticated caller could poll the online/offline status and last login timestamp of arbitrary users by username or user ID. The response also echoed back the resolved canonical user ID, enabling username-to-ID enumeration.

Resolution

Added session authentication requirement to the online-status endpoint. Requests without a valid session receive 401. Removed the resolved user ID field from the response body to prevent ID enumeration.

Reference:CWE-306

Our Security Commitment

  • All security findings are disclosed here within 24 hours of discovery
  • Critical and High severity issues are patched before any public disclosure
  • Regular third-party security audits conducted quarterly
  • No known security issues remain unpatched in production

Found a security issue? We appreciate responsible disclosure.

security@streamd.live