Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 15x 7x 7x 7x 7x 1x 1x 6x 6x 6x 6x 6x 7x 7x 2x 2x 4x 4x 2x 2x 2x 2x 2x 2x 2x | /**
* File Download Handler
*
* Serves files from within the workspace's site directory for browser download.
* Includes path traversal protection: only files under the site root are served.
*/
import * as fs from 'fs';
import * as path from 'path';
import type { Request, Response } from 'express';
import type { AppContainer } from '../../config/container.js';
/**
* Create a GET handler for file downloads restricted to the workspace site directory.
*
* Security:
* - Resolves the requested path with fs.realpath() to eliminate symlinks and .. sequences
* - Verifies the resolved path starts with the workspace's site root
* - Rejects paths outside the site directory with 403
*/
export function createFileDownloadHandler(container: AppContainer) {
return async (req: Request, res: Response) => {
const { siteKey, workspaceKey } = req.params;
const filePath = req.query.path;
Iif (typeof siteKey !== 'string' || typeof workspaceKey !== 'string') {
res.status(400).json({ error: 'Invalid site or workspace key' });
return;
}
if (typeof filePath !== 'string' || filePath.length === 0) {
res.status(400).json({ error: 'Missing or invalid path parameter' });
return;
}
try {
const workspaceService = await container.getWorkspaceService(siteKey, workspaceKey);
const siteRoot = workspaceService.getWorkspacePath();
// Resolve the site root to its real path (no symlinks)
const realSiteRoot = await fs.promises.realpath(siteRoot);
// If the path is relative, resolve it against the site root
const targetPath = path.isAbsolute(filePath)
? filePath
: path.join(realSiteRoot, filePath);
// Check the file exists before resolving realpath
try {
await fs.promises.access(targetPath, fs.constants.R_OK);
} catch {
res.status(404).json({ error: 'File not found' });
return;
}
// Resolve to real path (eliminates symlinks and .. sequences)
const realPath = await fs.promises.realpath(targetPath);
// Verify the resolved path is within the site root
if (!realPath.startsWith(realSiteRoot + path.sep) && realPath !== realSiteRoot) {
res.status(403).json({ error: 'Access denied' });
return;
}
// Stream the file as a download
const filename = path.basename(realPath);
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
const stream = fs.createReadStream(realPath);
stream.pipe(res);
stream.on('error', () => {
if (!res.headersSent) {
res.status(500).json({ error: 'Error reading file' });
}
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
res.status(500).json({ error: message });
}
};
}
|