A .mpcweb file carries each sample's opfs_path, and the import trusts it verbatim. That path is later passed to deleteFile and readFile, so a shared project file can point the app at any file in its own OPFS — including the database.
The path
Schema (src/core/project/mpcweb.ts):
Inserted unchanged (src/core/project/snapshotService.ts):
opfs_path: sample.opfs_path,
Consumed (src/features/browser/BrowserPanel.tsx, the "Purge unused samples" action):
await deleteFile(sample.opfs_path);
and read in src/core/audio/sampleEditService.ts:
const file = await readFile(row.opfs_path);
remapSnapshot only rewrites this field if it happens to contain an old UUID — an attacker simply omits the id from the path.
Concrete scenario
A shared .mpcweb sets samples[0].opfs_path = "/bangerbox.sqlite3" and gives the sample a name the user will plausibly purge. One purge or delete destroys the entire project database — every project, not just the imported one.
Scope of the impact
Directory escape is blocked: splitOpfsPath in src/core/storage/opfs.ts independently rejects .., . and empty segments, so this is confined to the origin's own OPFS and cannot reach the wider filesystem. That is why this is medium rather than critical — but "wipe the user's entire library" is still the worst outcome the app can produce.
Suggested fix
The writer already derives the real path from samplePath(projectId, newId) using a freshly generated UUID, so the imported value is redundant — the stored row and the actual file agree today only by coincidence. Either:
- Ignore the imported
opfs_path entirely and recompute it (preferred — removes the trust relationship rather than validating it), or
- Constrain it in the schema to the canonical
^/projects/<uuid>/samples/<uuid>\.wav$ shape.
Worth also reviewing whether deleteFile should refuse any path outside /projects/<id>/samples/ and /global_library/ as a defence in depth.
A
.mpcwebfile carries each sample'sopfs_path, and the import trusts it verbatim. That path is later passed todeleteFileandreadFile, so a shared project file can point the app at any file in its own OPFS — including the database.The path
Schema (
src/core/project/mpcweb.ts):Inserted unchanged (
src/core/project/snapshotService.ts):Consumed (
src/features/browser/BrowserPanel.tsx, the "Purge unused samples" action):and read in
src/core/audio/sampleEditService.ts:remapSnapshotonly rewrites this field if it happens to contain an old UUID — an attacker simply omits the id from the path.Concrete scenario
A shared
.mpcwebsetssamples[0].opfs_path = "/bangerbox.sqlite3"and gives the sample a name the user will plausibly purge. One purge or delete destroys the entire project database — every project, not just the imported one.Scope of the impact
Directory escape is blocked:
splitOpfsPathinsrc/core/storage/opfs.tsindependently rejects..,.and empty segments, so this is confined to the origin's own OPFS and cannot reach the wider filesystem. That is why this is medium rather than critical — but "wipe the user's entire library" is still the worst outcome the app can produce.Suggested fix
The writer already derives the real path from
samplePath(projectId, newId)using a freshly generated UUID, so the imported value is redundant — the stored row and the actual file agree today only by coincidence. Either:opfs_pathentirely and recompute it (preferred — removes the trust relationship rather than validating it), or^/projects/<uuid>/samples/<uuid>\.wav$shape.Worth also reviewing whether
deleteFileshould refuse any path outside/projects/<id>/samples/and/global_library/as a defence in depth.