Skip to content

Commit fd9b713

Browse files
committed
🤖 fix: validate indexed filenames before deleting sound assets
--- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `1.41`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=51.41 -->
1 parent 1fc9162 commit fd9b713

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

‎src/node/services/eventSoundAssetService.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,43 @@ describe("EventSoundAssetService", () => {
127127
expect(await service.listAssets()).toEqual([]);
128128
});
129129

130+
it("removes tampered index entries without deleting files outside the asset directory", async () => {
131+
const tamperedAssetId = "11111111-1111-1111-1111-111111111111.wav";
132+
const outsidePath = path.join(tempMuxHome, "outside.txt");
133+
await fsPromises.writeFile(outsidePath, "must-survive");
134+
135+
const assetsDirPath = path.join(tempMuxHome, "assets", "event-sounds");
136+
const indexPath = path.join(assetsDirPath, "index.json");
137+
await fsPromises.mkdir(assetsDirPath, { recursive: true });
138+
139+
await fsPromises.writeFile(
140+
indexPath,
141+
JSON.stringify(
142+
{
143+
version: 1,
144+
assets: {
145+
[tamperedAssetId]: {
146+
assetId: tamperedAssetId,
147+
fileName: "../../outside.txt",
148+
originalName: "tampered.wav",
149+
mimeType: "audio/wav",
150+
sizeBytes: 1,
151+
createdAt: new Date().toISOString(),
152+
},
153+
},
154+
},
155+
null,
156+
2
157+
),
158+
"utf-8"
159+
);
160+
161+
await service.deleteAsset(tamperedAssetId);
162+
163+
expect(await fsPromises.readFile(outsidePath, "utf-8")).toBe("must-survive");
164+
expect(await service.listAssets()).toEqual([]);
165+
});
166+
130167
it("treats deleteAsset for unknown ids as a no-op", async () => {
131168
await service.deleteAsset(KNOWN_MISSING_ASSET_ID);
132169
});

‎src/node/services/eventSoundAssetService.ts‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,20 @@ export class EventSoundAssetService {
266266
}
267267
}
268268

269+
private resolveIndexedAssetFilePath(fileName: string): string | null {
270+
if (!EVENT_SOUND_ASSET_ID_PATTERN.test(fileName)) {
271+
return null;
272+
}
273+
274+
const filePath = path.resolve(this.assetsDirPath, fileName);
275+
const relative = path.relative(this.assetsDirPath, filePath);
276+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
277+
return null;
278+
}
279+
280+
return filePath;
281+
}
282+
269283
private async storeAsset(params: {
270284
bytes: Buffer;
271285
originalName: string;
@@ -356,8 +370,15 @@ export class EventSoundAssetService {
356370
return;
357371
}
358372

359-
const filePath = path.join(this.assetsDirPath, entry.fileName);
360-
await fsPromises.rm(filePath, { force: true });
373+
const filePath = this.resolveIndexedAssetFilePath(entry.fileName);
374+
if (filePath) {
375+
await fsPromises.rm(filePath, { force: true });
376+
} else {
377+
log.warn("Skipping event sound asset file deletion for invalid indexed filename", {
378+
assetId,
379+
fileName: entry.fileName,
380+
});
381+
}
361382

362383
delete assets[assetId];
363384
await this.writeIndex(assets);
@@ -387,9 +408,8 @@ export class EventSoundAssetService {
387408
return null;
388409
}
389410

390-
const filePath = path.resolve(this.assetsDirPath, entry.fileName);
391-
const relative = path.relative(this.assetsDirPath, filePath);
392-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
411+
const filePath = this.resolveIndexedAssetFilePath(entry.fileName);
412+
if (!filePath) {
393413
return null;
394414
}
395415

0 commit comments

Comments
 (0)