diff options
Diffstat (limited to 'src/routes')
-rw-r--r-- | src/routes/api.ts | 287 |
1 files changed, 155 insertions, 132 deletions
diff --git a/src/routes/api.ts b/src/routes/api.ts index ee80ea5..22edd79 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -15,15 +15,16 @@ api.use(cookieParser()); const csrf = csurf({ cookie: true }); // For file uploads -api.use(fileUpload({ +api.use( + fileUpload({ preserveExtension: true, // Preserve file extension on upload safeFileNames: true, // Only allow alphanumeric characters in file names limits: { fileSize: 1 * 1024 * 1024 }, // Limit file size to 1MB useTempFiles: true, // Store files in temp instead of memory tempFileDir: '/tmp/', // Store files in /tmp/ debug: false, // Log debug information -})); - + }) +); /* Upload a file to the server @@ -44,34 +45,44 @@ api.use(fileUpload({ 415 when the file's MIME type is not text/x-python or text/plain (WINDOWS WHY) 500 for any other errors */ -api.route('/upload') - .post(csrf, (req: Request, res: Response) => { - try { - // Check if anything was actually uploaded - if (!req.files || Object.keys(req.files).length === 0) - return res.status(400).json({ error: 'No file uploaded.' }); - - const file: UploadedFile = req.files.file as UploadedFile; // Kludge to prevent a compiler error, only one file gets uploaded so this should be fine - - // Check if the file is too large (see fileUpload.limits for the limit) - if (file.truncated) - return res.status(413).json({ error: 'File uploaded was too large.' }); - - // Check if the file is a python file - if (file.mimetype !== 'text/x-python' && file.mimetype !== 'text/plain') - return res.status(415).json({ error: 'File uploaded was not a Python file.' }); - - res.status(201).json({ file: { file: file.name, path: file.tempFilePath }, msg: 'File uploaded successfully.', csrf: req.csrfToken() }); - } catch (err) { - // Generic error handler - res.status(500).json({ error: 'An unknown error occurred while uploading the file.', error_msg: err }); - } - }) - // Fallback - .all(csrf, (req: Request, res: Response) => { - res.set('Allow', 'POST'); - res.status(405).json({ error: 'Method not allowed.' }); - }); +api + .route('/upload') + .post(csrf, (req: Request, res: Response) => { + try { + // Check if anything was actually uploaded + if (!req.files || Object.keys(req.files).length === 0) + return res.status(400).json({ error: 'No file uploaded.' }); + + const file: UploadedFile = req.files.file as UploadedFile; // Kludge to prevent a compiler error, only one file gets uploaded so this should be fine + + // Check if the file is too large (see fileUpload.limits for the limit) + if (file.truncated) + return res.status(413).json({ error: 'File uploaded was too large.' }); + + // Check if the file is a python file + if (file.mimetype !== 'text/x-python' && file.mimetype !== 'text/plain') + return res + .status(415) + .json({ error: 'File uploaded was not a Python file.' }); + + res.status(201).json({ + file: { file: file.name, path: file.tempFilePath }, + msg: 'File uploaded successfully.', + csrf: req.csrfToken(), + }); + } catch (err) { + // Generic error handler + res.status(500).json({ + error: 'An unknown error occurred while uploading the file.', + error_msg: err, + }); + } + }) + // Fallback + .all(csrf, (req: Request, res: Response) => { + res.set('Allow', 'POST'); + res.status(405).json({ error: 'Method not allowed.' }); + }); /* Actuate the pendulum @@ -102,20 +113,20 @@ api.route('/upload') Minimizing PE vectors like running this as an extremely low privilege user is a must. */ -api.route('/actuate') - // Snyk error mitigation, should be fine since the rate limiting is already in place - // file deepcode ignore NoRateLimitingForExpensiveWebOperation: This is already rate limited by the website, so we don't need to do it again - .post(csrf, async (req: Request, res: Response) => { - try { - const path: string = req.body.file.path; - // Verify that the file exists and is a regular file - // Return if not since the res will be sent by the verifyFile function - if (await verifyFile(path, res) !== true) - return; - - const escaped = quote([ path ]); - // Run the code - /* +api + .route('/actuate') + // Snyk error mitigation, should be fine since the rate limiting is already in place + // file deepcode ignore NoRateLimitingForExpensiveWebOperation: This is already rate limited by the website, so we don't need to do it again + .post(csrf, async (req: Request, res: Response) => { + try { + const path: string = req.body.file.path; + // Verify that the file exists and is a regular file + // Return if not since the res will be sent by the verifyFile function + if ((await verifyFile(path, res)) !== true) return; + + const escaped = quote([path]); + // Run the code + /* TODO: - Potentially add the limiter to one-per-person here - Add a timeout @@ -123,37 +134,48 @@ api.route('/actuate') - Make this more secure - HOW? */ - // let output = ''; - let stderr = ''; - const actuation = spawn('python', escaped.split(' ')); - // actuation.stdout.on('data', (data: Buffer) => { - // output += data.toString(); - // }); - actuation.stderr.on('data', (data: Buffer) => { - stderr += `STDERR: ${data.toString()}`; - }); - actuation.on('close', (code: number) => { - const filename: string = (req.body.file.path as string).split('/').pop() as string; - // Make sure the program exited with a code of 0 (success) - if (code !== 0) - return res.status(500).json({ error: `Program exited with exit code ${code}`, error_msg: stderr, file: { name: req.body.file.file, filename: filename } }); - return res.status(200).json({ file: { name: req.body.file.file, filename: filename } }); - }); - // Kill the process if it takes too long - // Default timeout is 120 seconds (2 minutes) - setTimeout(() => { - actuation.kill(); - }, 120000); - } catch (err) { - // Generic error handler - return res.status(500).json({ error: 'An unknown error occurred while running the file.', error_msg: err }); - } - }) - // Fallback - .all(csrf, (req: Request, res: Response) => { - res.set('Allow', 'POST'); - return res.status(405).json({ error: 'Method not allowed.' }); - }); + // let output = ''; + let stderr = ''; + const actuation = spawn('python', escaped.split(' ')); + // actuation.stdout.on('data', (data: Buffer) => { + // output += data.toString(); + // }); + actuation.stderr.on('data', (data: Buffer) => { + stderr += `STDERR: ${data.toString()}`; + }); + actuation.on('close', (code: number) => { + const filename: string = (req.body.file.path as string) + .split('/') + .pop() as string; + // Make sure the program exited with a code of 0 (success) + if (code !== 0) + return res.status(500).json({ + error: `Program exited with exit code ${code}`, + error_msg: stderr, + file: { name: req.body.file.file, filename: filename }, + }); + return res + .status(200) + .json({ file: { name: req.body.file.file, filename: filename } }); + }); + // Kill the process if it takes too long + // Default timeout is 120 seconds (2 minutes) + setTimeout(() => { + actuation.kill(); + }, 120000); + } catch (err) { + // Generic error handler + return res.status(500).json({ + error: 'An unknown error occurred while running the file.', + error_msg: err, + }); + } + }) + // Fallback + .all(csrf, (req: Request, res: Response) => { + res.set('Allow', 'POST'); + return res.status(405).json({ error: 'Method not allowed.' }); + }); /* Download the CSV file after running the pendulum @@ -166,35 +188,34 @@ api.route('/actuate') 404 when the file is not accessible or does not exist 500 for any other errors */ -api.route('/download') - .get(csrf, async (req: Request, res: Response) => { - const filename: string = req.query.filename as string; - if (!filename) - return res.status(400).json({ error: 'No filename specified.' }); - // Make sure no path traversal is attempted - // This regex matches all alphanumeric characters, underscores, and dashes. - // MAKE SURE THIS DOES NOT ALLOW PATH TRAVERSAL - if (!/^[\w-]+$/.test(filename)) - return res.status(403).json({ error: 'No.' }); - - const path = `/tmp/${filename}.csv`; - - // Verify that the file exists and is a regular file - // Return if not since the res will be sent by the verifyFile function - if (await verifyFile(path, res) !== true) - return; - // Read the file and send it to the client - res.type('text/csv'); - // Snyk error mitigation, should be fine since tmp is private and the simple regex above should prevent path traversal - // deepcode ignore PT: This is probably mitigated by the regex - return res.sendFile(path); - }) - // Fallback - .all(csrf, (req: Request, res: Response) => { - res.set('Allow', 'GET'); - return res.status(405).json({ error: 'Method not allowed.' }); - }); +api + .route('/download') + .get(csrf, async (req: Request, res: Response) => { + const filename: string = req.query.filename as string; + if (!filename) + return res.status(400).json({ error: 'No filename specified.' }); + // Make sure no path traversal is attempted + // This regex matches all alphanumeric characters, underscores, and dashes. + // MAKE SURE THIS DOES NOT ALLOW PATH TRAVERSAL + if (!/^[\w-]+$/.test(filename)) + return res.status(403).json({ error: 'No.' }); + + const path = `/tmp/${filename}.csv`; + // Verify that the file exists and is a regular file + // Return if not since the res will be sent by the verifyFile function + if ((await verifyFile(path, res)) !== true) return; + // Read the file and send it to the client + res.type('text/csv'); + // Snyk error mitigation, should be fine since tmp is private and the simple regex above should prevent path traversal + // deepcode ignore PT: This is probably mitigated by the regex + return res.sendFile(path); + }) + // Fallback + .all(csrf, (req: Request, res: Response) => { + res.set('Allow', 'GET'); + return res.status(405).json({ error: 'Method not allowed.' }); + }); /* Verify that the file exists and is a regular file @@ -207,36 +228,38 @@ api.route('/download') ** AFTER THIS POINT, THE API HAS ALREADY SENT A RESPONSE, SO THE FUNCTION THAT CALLED IT SHOULD NOT RETURN ANOTHER RESPONSE ** */ async function verifyFile(file: string, res: Response) { - // Make sure the file being requested to run exists - try { - await access(file); - } catch (err) { - res.status(404).json({ error: 'File is not accessible or does not exist.' }); - return false; - } - // This is a try catch because otherwise type checking will fail and get all messed up - // Handle your promise rejections, kids - try { - const stats = await stat(file); - // Make sure the file being requested to run is a regular file - if (!stats.isFile()) { - res.status(400).json({ error: 'File is not a regular file.' }); - return false; - } - // Make sure the file being requested to run is not a directory - else if (stats.isDirectory()) { - res.status(400).json({ error: 'File is a directory.' }); - return false; - } - - - // File does exist and is a regular file, so it is good to go - return true; + // Make sure the file being requested to run exists + try { + await access(file); + } catch (err) { + res + .status(404) + .json({ error: 'File is not accessible or does not exist.' }); + return false; + } + // This is a try catch because otherwise type checking will fail and get all messed up + // Handle your promise rejections, kids + try { + const stats = await stat(file); + // Make sure the file being requested to run is a regular file + if (!stats.isFile()) { + res.status(400).json({ error: 'File is not a regular file.' }); + return false; } - catch (err) { - res.status(404).json({ error: 'File is not accessible or does not exist.' }); - return false; + // Make sure the file being requested to run is not a directory + else if (stats.isDirectory()) { + res.status(400).json({ error: 'File is a directory.' }); + return false; } + + // File does exist and is a regular file, so it is good to go + return true; + } catch (err) { + res + .status(404) + .json({ error: 'File is not accessible or does not exist.' }); + return false; + } } export default api; |