#201·waoowaoo

Private media can be accessed without ownership checks via stable /m IDs

Author: 3em0Created Jun 10, 2026Updated Jun 10, 2026

Summary

Private uploaded/generated media can be retrieved through stable media routes without any user or project ownership check.

The application derives a stable public media ID from only the storage key:

typescript
stablePublicIdFromStorageKey(storageKey) = "m_" + sha256(storageKey).slice(0, 40)

MediaObject records are global and contain publicId and storageKey, but no userId, tenantId, or project owner field. The /m/[publicId] route then looks up the global media row by publicId, signs media.storageKey, and streams the object without requiring authentication.

Affected code

  • src/lib/media/hash.ts
    • stablePublicIdFromStorageKey(storageKey) derives the public ID only from storageKey.
  • prisma/schema.prisma
    • MediaObject has globally unique publicId and storageKey, but no owner/security-scope field.
  • src/lib/media/service.ts
    • ensureMediaObjectFromStorageKey() and getMediaObjectByPublicId() create and resolve media globally by storageKey/publicId.
  • src/app/m/[publicId]/route.ts
    • GET and HEAD retrieve media by publicId without authentication or ownership checks.
  • src/app/api/storage/sign/route.ts
    • GET /api/storage/sign?key=... redirects to a signed object URL for any supplied key without authentication, which broadens the same access-control concern.

Impact

If an attacker learns or can derive a victim media storageKey or /m/<publicId> URL, the attacker can retrieve the victim's media without logging in or owning the related project/asset.

Exposed data may include user-uploaded or AI-generated images, videos, audio, voice samples, character assets, scene assets, and other project media. This is primarily a confidentiality issue and unauthorized media reuse issue. Direct remote code execution was not observed.

Reproduction approach

Prerequisites:

  • The app is reachable over the network.
  • A victim has an existing media object for a known or leaked storage key, for example uploads/cat.png.

Steps:

  1. Compute the stable public ID:

    bash
    node -e "const c=require('node:crypto'); const k='uploads/cat.png'; console.log('m_'+c.createHash('sha256').update(k).digest('hex').slice(0,40))"

    This produces:

    m_61528aebee09284f2c313f9751fcfba79511c259
  2. Request the media route without a session:

    bash
    curl -i "http://HOST:PORT/m/m_61528aebee09284f2c313f9751fcfba79511c259"
  3. The route resolves the global MediaObject by publicId, signs the stored storageKey, and streams the object without checking whether the caller owns the referenced media.

If the storage key itself is known, the unauthenticated signing endpoint can also be used directly:

bash
curl -i "http://HOST:PORT/api/storage/sign?key=uploads%2Fcat.png"

Expected behavior

Media retrieval should enforce the same user/project ownership model as the project and asset APIs, or the public media identifier should be an unguessable bearer token scoped to the intended access policy.

Actual behavior

The media route uses a deterministic ID based only on storageKey and does not bind the lookup to user, tenant, project, or asset ownership.

Severity

Medium to High, depending on deployment:

  • Higher for public multi-user deployments where user-generated media is expected to be private.
  • Lower for strictly single-user localhost-only deployments.

Attack complexity depends on the attacker's ability to obtain or infer a valid storage key or /m URL. Once the identifier is known, retrieval is reliable and requires no authentication.