diff --git a/workflow/packages/backend/worker/src/lib/block-manager/development/blocks-builder.ts b/workflow/packages/backend/worker/src/lib/block-manager/development/blocks-builder.ts index 1ccc5fd7..481962dd 100644 --- a/workflow/packages/backend/worker/src/lib/block-manager/development/blocks-builder.ts +++ b/workflow/packages/backend/worker/src/lib/block-manager/development/blocks-builder.ts @@ -62,7 +62,12 @@ async function runCommandWithLiveOutput(cmd: string): Promise { const [command, ...args] = cmd.split(' ') return new Promise((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 => { diff --git a/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.test.ts b/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.test.ts new file mode 100644 index 00000000..e980989d --- /dev/null +++ b/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.test.ts @@ -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'); + }); +}); diff --git a/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.ts b/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.ts index fe4c0a20..25059341 100644 --- a/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.ts +++ b/workflow/packages/blocks/community/aixblock/src/lib/actions/model/preview-server.ts @@ -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 { - 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 { + 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'); + } }