Private media can be accessed without ownership checks via stable /m IDs
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:
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.tsstablePublicIdFromStorageKey(storageKey)derives the public ID only fromstorageKey.
prisma/schema.prismaMediaObjecthas globally uniquepublicIdandstorageKey, but no owner/security-scope field.
src/lib/media/service.tsensureMediaObjectFromStorageKey()andgetMediaObjectByPublicId()create and resolve media globally bystorageKey/publicId.
src/app/m/[publicId]/route.tsGETandHEADretrieve media bypublicIdwithout authentication or ownership checks.
src/app/api/storage/sign/route.tsGET /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:
Compute the stable public ID:
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_61528aebee09284f2c313f9751fcfba79511c259Request the media route without a session:
curl -i "http://HOST:PORT/m/m_61528aebee09284f2c313f9751fcfba79511c259"The route resolves the global
MediaObjectbypublicId, signs the storedstorageKey, 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:
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.
Source: waooAI/waoowaoo