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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
import express, { Request, Response } from 'express';
// Middleware for security
import csurf from 'csurf';
import cookieParser from 'cookie-parser';
import fileUpload, { UploadedFile } from 'express-fileupload';
// For executing the python scripts
import { access, stat } from 'fs/promises';
import { Stats } from 'fs';
import { quote } from 'shell-quote';
import { spawn } from 'child_process';
const api = express.Router();
// Use JSON parser for API requests and responses
api.use(express.json());
// CSRF protection
api.use(cookieParser());
const csrf = csurf({ cookie: true });
// 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
}));
/*
Upload a file to the server
POST /api/v1/upload
Parameters:
files: The file to upload
Returns:
201:
{
"message": "File uploaded successfully",
"file": {
"name": "file.py",
"path": "/tmp/file-538126",
}
}
400 when there is no file
413 when the file is too large
415 when the file's MIME type is not text/x-python
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')
return res.status(415).json({ error: 'File uploaded was not a Python file.' });
res.status(201).json({ 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
POST /api/v1/actuate
Parameters:
name: The name of the file to run, currently unused
path: The path to the uploaded file on the server, passed in from /api/v1/upload on the website
Returns:
200:
{
"stdout": "Hello from Python!\n",
}
400 for when the file passed in is not a regular file
403 when the file is not accessible
500:
{
"error": "Program exited with error code 1.",
"error_msg": "NameError: name 'sleep' is not defined",
}
This route is probably a complete security nightmare. It allows anyone to run arbitrary Python code on the server.
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) => {
// 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 or does not exist.' });
}
const stats: Stats = await stat(req.body.path);
// Make sure the file being requested to run is a regular file
if (!stats.isFile())
return res.status(400).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(400).json({ error: 'File is a directory.' });
const escaped = quote( [ req.body.path ] );
// Run the code
/*
TODO:
- Potentially add the limiter to one-per-person here
- Add a timeout
- Communicate to the machine to give up (the user as well), maybe just kill the process?
- Make this more secure
- HOW?
*/
let output = '';
const actuation = spawn('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: `Program exited with exit code ${code}`, 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;
|