aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/api.ts
blob: eb2096374e7d1aa67575fc2fbef39f85dfb443fc (plain) (blame)
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;