From 26b391452a7a629fb28d85caac3f8eba95cc1ddd Mon Sep 17 00:00:00 2001 From: Matt Strapp Date: Mon, 14 Feb 2022 20:08:16 -0600 Subject: Make responses more clear Signed-off-by: Matt Strapp --- src/public/css/style.css | 5 +++- src/public/js/form.js | 13 ++++++++-- src/public/lib/prism.css | 3 --- src/public/lib/prism.js | 4 --- src/routes/api.ts | 64 ++++++++++++++++++++++++++++++++++++++++------- src/views/pages/index.ejs | 6 ++++- 6 files changed, 75 insertions(+), 20 deletions(-) delete mode 100644 src/public/lib/prism.css delete mode 100644 src/public/lib/prism.js (limited to 'src') diff --git a/src/public/css/style.css b/src/public/css/style.css index d48e8b9..b1651df 100644 --- a/src/public/css/style.css +++ b/src/public/css/style.css @@ -117,7 +117,10 @@ a { text-decoration: none; } +h2 { + font-size: larger; +} + .error { color: red; - font-size: larger; } \ No newline at end of file diff --git a/src/public/js/form.js b/src/public/js/form.js index f58a27f..581d5a3 100644 --- a/src/public/js/form.js +++ b/src/public/js/form.js @@ -4,10 +4,12 @@ window.onload = function () { }; // File submit AJAX request +// After successful upload, actuate the file by calling actuate() on the successfully uploaded file document.getElementById('upload').onsubmit = function () { - // Reset error message + // Reset error message and success message document.getElementById('upload-err').innerText = ''; document.getElementById('actuate-err').innerText = ''; + document.getElementById('upload-response').innerText = ''; // Make AJAX request let xhr = new XMLHttpRequest(); xhr.open('POST', '/api/v1/upload'); @@ -16,7 +18,9 @@ document.getElementById('upload').onsubmit = function () { xhr.onreadystatechange = function () { if (xhr.readyState === 4) { let response = JSON.parse(xhr.responseText); - if (xhr.status === 200) { + if (xhr.status === 201) { + // Display upload success message to the user + document.getElementById('upload-response').innerText = response.msg; actuate(response); } else { // Display upload error message to the user @@ -27,9 +31,14 @@ document.getElementById('upload').onsubmit = function () { } } }; + document.getElementById('upload').reset(); return false; }; +// Actuate button AJAX request +// Should always be called after upload +// Implies that upload has been successful since it relies on the upload response +// function actuate(file) { let xhr = new XMLHttpRequest(); xhr.open('POST', '/api/v1/actuate'); diff --git a/src/public/lib/prism.css b/src/public/lib/prism.css deleted file mode 100644 index 4fb0a7c..0000000 --- a/src/public/lib/prism.css +++ /dev/null @@ -1,3 +0,0 @@ -/* PrismJS 1.26.0 -https://prismjs.com/download.html#themes=prism-okaidia&languages=python */ -code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} diff --git a/src/public/lib/prism.js b/src/public/lib/prism.js deleted file mode 100644 index 958c4c6..0000000 --- a/src/public/lib/prism.js +++ /dev/null @@ -1,4 +0,0 @@ -/* PrismJS 1.26.0 -https://prismjs.com/download.html#themes=prism-okaidia&languages=python */ -var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var t=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,e={},M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=l.reach);y+=m.value.length,m=m.next){var k=m.value;if(t.length>n.length)return;if(!(k instanceof W)){var x,b=1;if(h){if(!(x=z(p,y,n,f))||x.index>=n.length)break;var w=x.index,A=x.index+x[0].length,E=y;for(E+=m.value.length;E<=w;)m=m.next,E+=m.value.length;if(E-=m.value.length,y=E,m.value instanceof W)continue;for(var P=m;P!==t.tail&&(El.reach&&(l.reach=j);var C=m.prev;S&&(C=I(t,C,S),y+=S.length),T(t,C,b);var N=new W(o,g?M.tokenize(L,g):L,d,L);if(m=I(t,C,N),O&&I(t,m,O),1l.reach&&(l.reach=_.reach)}}}}}}(e,a,n,a.head,0),function(e){var n=[],t=e.head.next;for(;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=M.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=M.hooks.all[e];if(t&&t.length)for(var r,a=0;r=t[a++];)r(n)}},Token:W};function W(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function z(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function i(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function I(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function T(e,n,t){for(var r=n.next,a=0;a"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var r=M.util.currentScript();function a(){M.manual||M.highlightAll()}if(r&&(M.filename=r.src,r.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var l=document.readyState;"loading"===l||"interactive"===l&&r&&r.defer?document.addEventListener("DOMContentLoaded",a):window.requestAnimationFrame?window.requestAnimationFrame(a):window.setTimeout(a,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); -Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; diff --git a/src/routes/api.ts b/src/routes/api.ts index bae30b0..015bd45 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,9 +1,12 @@ import express, { Request, Response } from 'express'; +// Middleware for security import csurf from 'csurf'; import cookieParser from 'cookie-parser'; import fileUpload, { UploadedFile } from 'express-fileupload'; import rateLimit from 'express-rate-limit'; +// 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'; @@ -32,7 +35,28 @@ api.use(rateLimiter); api.use(cookieParser()); const csrf = csurf({ cookie: true }); +// Use JSON parser for API requests and responses api.use(express.json()); + +/* + 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 { @@ -50,7 +74,7 @@ api.route('/upload') 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() }); + 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 }); @@ -63,9 +87,28 @@ api.route('/upload') }); /* - This route is probably a complete security hole. It allows anyone with a login cookie access to run arbitrary Python code on the server. + 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. - Minimizing PE vectors like running this as a low privilege user is a must. */ api.route('/actuate') .post(csrf, async (req: Request, res: Response) => { @@ -77,7 +120,7 @@ api.route('/actuate') } - const stats = await stat(req.body.path); + 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.' }); @@ -88,12 +131,15 @@ api.route('/actuate') const escaped = quote( [ req.body.path ] ); // Run the code /* - TODO: MAKE THIS MORE SECURE + 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 = ''; - // NOT PORTABLE: ASSUMES PYTHON 3 IS THERE AS WELL AS ON UNIX - // TODO: MAKE PORTABLE - const actuation = spawn('/usr/bin/python', escaped.split(' ')); + const actuation = spawn('python', escaped.split(' ')); actuation.stdout.on('data', (data: Buffer) => { output += data.toString(); }); @@ -102,7 +148,7 @@ api.route('/actuate') }); 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(500).json({ error: `Program exited with exit code ${code}`, error_msg: output }); res.status(200).json({ stdout: output }); }); }) diff --git a/src/views/pages/index.ejs b/src/views/pages/index.ejs index 81624f6..357baae 100644 --- a/src/views/pages/index.ejs +++ b/src/views/pages/index.ejs @@ -30,7 +30,11 @@ - +

+
+

+ + -- cgit v1.2.3