Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ async function runCommandWithLiveOutput(cmd: string): Promise<void> {
const [command, ...args] = cmd.split(' ')

return new Promise<void>((resolve, reject) => {
const child = spawn(command, args, { stdio: 'inherit', shell: true })
// const child = spawn(command, args, { stdio: 'inherit', shell: true })
// Without shell: true : Input validation
if (!/^[a-zA-Z0-9_-]+$/.test(command)) {
throw new Error("Invalid command");
}
const child = spawn(command, args, { stdio: 'inherit' }); // Without shell: true

child.on('error', reject)
child.on('close', code => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import request from 'supertest';
import express from 'express';
import { servePreviewFile } from './preview-server';

// Set up a minimal Express app for testing
const app = express();
app.get('/preview', servePreviewFile);

describe('servePreviewFile', () => {
it('should serve a valid file', async () => {
const res = await request(app).get('/preview?file=test.png');
expect(res.status).toBe(200);
expect(res.headers['content-type']).toMatch(/(png|jpeg|gif|pdf|txt)/);
});

it('should reject missing file parameter', async () => {
const res = await request(app).get('/preview');
expect(res.status).toBe(400);
expect(res.text).toContain('Missing or invalid');
});

it('should reject path traversal attempts', async () => {
const maliciousPaths = [
'../../../../etc/passwd',
'../../../.env',
'test.png/../../../.env',
'nonexistent/../../package.json',
'valid.png%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd'
];

for (const payload of maliciousPaths) {
const res = await request(app).get(`/preview?file=${encodeURIComponent(payload)}`);
expect(res.status).toBe(400);
expect(res.text).toContain('Invalid file path');
}
});

it('should reject disallowed file extensions', async () => {
const res = await request(app).get('/preview?file=malicious.js');
expect(res.status).toBe(400);
expect(res.text).toContain('File type not allowed');
});

it('should return 404 for non-existent valid files', async () => {
const res = await request(app).get('/preview?file=missing.pdf');
expect(res.status).toBe(404);
expect(res.text).toContain('File not found');
});
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
import express from 'express';
import path from 'path';
import { Request, Response } from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

export function startPreviewServer(htmlContent: string): Promise<number> {
return new Promise((resolve) => {
// Write HTML to temp file
const tempDir = os.tmpdir();
const tempFile = path.join(tempDir, 'aixblock-monitoring.html');
fs.writeFileSync(tempFile, htmlContent);

// Start local server
const app = express();
app.get('/', (req: Request, res: Response) => {
res.sendFile(tempFile);
});

const server = app.listen(0, () => {
const port = (server.address() as any).port;
console.log(`Preview server running on port ${port}`);
resolve(port);
});
});

// Define the safe base directory where preview files are stored.
// This directory must be controlled and not user-writable.
const SAFE_PREVIEW_DIR = path.resolve(__dirname, '../../../../../safe-preview-files');

/**
* Validates that the requested file path is within the SAFE_PREVIEW_DIR.
* Prevents path traversal attacks (e.g., ../../etc/passwd).
*/
function isValidPreviewPath(filePath: string): boolean {
if (typeof filePath !== 'string') return false;
const resolved = path.resolve(SAFE_PREVIEW_DIR, filePath);
// Ensure the resolved path starts with SAFE_PREVIEW_DIR + path.sep to prevent directory escape
return resolved.startsWith(SAFE_PREVIEW_DIR + path.sep);
}

/**
* Serves a preview file securely.
* Only files inside SAFE_PREVIEW_DIR with allowed extensions are served.
*/
export async function servePreviewFile(req: Request, res: Response): Promise<void> {
const { file: tempFile } = req.query;

// Validate presence and type of 'file' parameter
if (!tempFile || typeof tempFile !== 'string') {
return res.status(400).send('Missing or invalid "file" parameter');
}

// Block path traversal attempts
if (!isValidPreviewPath(tempFile)) {
return res.status(400).send('Invalid file path');
}

const resolvedPath = path.resolve(SAFE_PREVIEW_DIR, tempFile);

// Allow only safe file extensions to prevent execution of malicious content
const allowedExts = ['.png', '.jpg', '.jpeg', '.gif', '.pdf', '.txt'];
const ext = path.extname(resolvedPath).toLowerCase();
if (!allowedExts.includes(ext)) {
return res.status(400).send('File type not allowed');
}

try {
await res.sendFile(resolvedPath);
} catch (error: any) {
if (error.code === 'ENOENT') {
return res.status(404).send('File not found');
}
return res.status(500).send('Internal server error');
}
}