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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
import express, { Request, Response } from 'express';
import csurf from 'csurf';
import cookieParser from 'cookie-parser';
import fileUpload, { UploadedFile } from 'express-fileupload';
import rateLimit from 'express-rate-limit';
import { access, stat } from 'fs/promises';
import { quote } from 'shell-quote';
import { spawn } from 'child_process';
const api = express.Router();
// For file uploads
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
}));
// Slow down frequent requests to prevent DoS attacks
const rateLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 10, // Limit each IP to 10 requests per `window` (here, per 1 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
api.use(rateLimiter);
// CSRF protection
api.use(cookieParser());
const csrf = csurf({ cookie: true });
api.use(express.json());
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')
return res.status(415).json({ error: 'File uploaded was not a Python file.' });
res.status(200).json({ file: file.name, path: file.tempFilePath, 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.' });
});
/*
This route is probably a complete security hole. It allows anyone with a login cookie access to run arbitrary Python code on the server.
Minimizing PE vectors like running this as a low privilege user is a must.
*/
api.route('/actuate')
.post(csrf, async (req: Request, res: Response) => {
// Make sure the file being requested to run exists
try {
await access(req.body.path);
} catch (err) {
return res.status(403).json({ error: 'File is not accessible.' });
}
const stats = await stat(req.body.path);
// Make sure the file being requested to run is a regular file
if (!stats.isFile())
return res.status(403).json({ error: 'File is not a regular file.' });
// Make sure the file being requested to run is not a directory
if (stats.isDirectory())
return res.status(403).json({ error: 'File is a directory.' });
const escaped = quote( [ req.body.path ] );
// Run the code
/*
TODO: MAKE THIS MORE SECURE
*/
let output = '';
// NOT PORTABLE: ASSUMES PYTHON 3 IS THERE AS WELL AS ON UNIX
// TODO: MAKE PORTABLE
const actuation = spawn('/usr/bin/python', escaped.split(' '));
actuation.stdout.on('data', (data: Buffer) => {
output += data.toString();
});
actuation.stderr.on('data', (data: Buffer) => {
output += `STDERR: ${data.toString()}`;
});
actuation.on('close', (code: number) => {
if (code !== 0)
res.status(500).json({ error: 'An unknown error occurred while running the file.', error_msg: output });
res.status(200).json({ stdout: output });
});
})
// Fallback
.all(csrf, (req: Request, res: Response) => {
res.set('Allow', 'POST');
res.status(405).json({ error: 'Method not allowed.' });
});
export default api;
|