diff --git a/src/media/store.redirect.test.ts b/src/media/store.redirect.test.ts index fd07ce69005..ae6b0f10cac 100644 --- a/src/media/store.redirect.test.ts +++ b/src/media/store.redirect.test.ts @@ -89,6 +89,9 @@ describe("media store redirects", () => { expect(saved.contentType).toBe("text/plain"); expect(path.extname(saved.path)).toBe(".txt"); expect(await fs.readFile(saved.path, "utf8")).toBe("redirected"); + const stat = await fs.stat(saved.path); + const expectedMode = process.platform === "win32" ? 0o666 : 0o644; + expect(stat.mode & 0o777).toBe(expectedMode); }); it("fails when redirect response omits location header", async () => { diff --git a/src/media/store.ts b/src/media/store.ts index 9bfe481c93d..9dc6f5f641b 100644 --- a/src/media/store.ts +++ b/src/media/store.ts @@ -14,6 +14,9 @@ const resolveMediaDir = () => path.join(resolveConfigDir(), "media"); export const MEDIA_MAX_BYTES = 5 * 1024 * 1024; // 5MB default const MAX_BYTES = MEDIA_MAX_BYTES; const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes +// Files are intentionally readable by non-owner UIDs so Docker sandbox containers can access +// inbound media. The containing state/media directories remain 0o700, which is the trust boundary. +const MEDIA_FILE_MODE = 0o644; type RequestImpl = typeof httpRequest; type ResolvePinnedHostnameImpl = typeof resolvePinnedHostname; @@ -170,7 +173,7 @@ async function downloadToFile( let total = 0; const sniffChunks: Buffer[] = []; let sniffLen = 0; - const out = createWriteStream(dest, { mode: 0o600 }); + const out = createWriteStream(dest, { mode: MEDIA_FILE_MODE }); res.on("data", (chunk) => { total += chunk.length; if (sniffLen < 16384) { @@ -284,7 +287,7 @@ export async function saveMediaSource( const ext = extensionForMime(mime) ?? path.extname(source); const id = ext ? `${baseId}${ext}` : baseId; const dest = path.join(dir, id); - await fs.writeFile(dest, buffer, { mode: 0o600 }); + await fs.writeFile(dest, buffer, { mode: MEDIA_FILE_MODE }); return { id, path: dest, size: stat.size, contentType: mime }; } catch (err) { if (err instanceof SafeOpenError) { @@ -323,6 +326,6 @@ export async function saveMediaBuffer( } const dest = path.join(dir, id); - await fs.writeFile(dest, buffer, { mode: 0o600 }); + await fs.writeFile(dest, buffer, { mode: MEDIA_FILE_MODE }); return { id, path: dest, size: buffer.byteLength, contentType: mime }; }