diff --git a/source/_posts/How-to-create-static-HTTP-server-in-Node-JS.md b/source/_posts/How-to-create-static-HTTP-server-in-Node-JS.md index 670f232..25d82ec 100644 --- a/source/_posts/How-to-create-static-HTTP-server-in-Node-JS.md +++ b/source/_posts/How-to-create-static-HTTP-server-in-Node-JS.md @@ -103,7 +103,7 @@ But what if that index file doesn't exist? We will then need to serve 404 error console.log("Started server at port " + port + "."); }); {% endcodeblock %} -Note, that in static HTTPS servers, files are determined from resource URL. +Note, that in static HTTP servers, files are determined from resource URL. {% codeblock server.js lang:javascript %} //WARNING!!! PATH TRAVERSAL var http = require("http"); @@ -111,7 +111,7 @@ Note, that in static HTTPS servers, files are determined from resource URL. var port = 8080; var server = http.createServer(function (req, res) { var filename = "." + req.url; - if(req.url == "/") filename = "./index.html"; + if(filename == "./") filename = "./index.html"; fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { @@ -138,7 +138,7 @@ Note, that in static HTTPS servers, files are determined from resource URL. console.log("Started server at port " + port + "."); }); {% endcodeblock %} -But we have introduced path traversal vulnerability! (being able to access file outside the web root) To mitigate that, we'll use a regular expression, that removes all dot-dot-slash sequences from file name: +But we have introduced path traversal vulnerability (being able to access file outside the web root)! To mitigate that, we'll use a regular expression, that removes all dot-dot-slash sequences from file name: {% codeblock server.js lang:javascript %} //Path traversal mitigated var http = require("http"); @@ -146,8 +146,8 @@ But we have introduced path traversal vulnerability! (being able to access file var port = 8080; var server = http.createServer(function (req, res) { var filename = "." + req.url; - filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"").replace(/\/+/g,"/"); //Poor mans URL sanitizer - if(req.url == "/") filename = "./index.html"; + filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"/").replace(/\/+/g,"/"); //Poor mans URL sanitizer + if(filename == "./") filename = "./index.html"; fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { @@ -184,8 +184,8 @@ That might work fine for HTML files, but if you try other files, there will be c var port = 8080; var server = http.createServer(function (req, res) { var filename = "." + req.url; - filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"").replace(/\/+/g,"/"); //Poor mans URL sanitizer - if(req.url == "/") filename = "./index.html"; + filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"/").replace(/\/+/g,"/"); //Poor mans URL sanitizer + if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. fs.readFile(filename, function(err, data) { if(err) { @@ -224,8 +224,8 @@ But with query strings, it will fail. To prevent that, we'll be using WHATWG URL var server = http.createServer(function (req, res) { var urlObject = new URL(req.url, "http://localhost"); var filename = "." + urlObject.pathname; - filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"").replace(/\/+/g,"/"); //Poor mans URL sanitizer - if(req.url == "/") filename = "./index.html"; + filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"/").replace(/\/+/g,"/"); //Poor mans URL sanitizer + if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. fs.readFile(filename, function(err, data) { if(err) { @@ -274,8 +274,8 @@ It's nearly finished! But encoded URLs will not work. To fix that, we will use ` res.end("400 Bad Request"); return; } - filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"").replace(/\/+/g,"/"); //Poor mans URL sanitizer - if(req.url == "/") filename = "./index.html"; + filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"/").replace(/\/+/g,"/"); //Poor mans URL sanitizer + if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. fs.readFile(filename, function(err, data) { if(err) { @@ -303,6 +303,64 @@ It's nearly finished! But encoded URLs will not work. To fix that, we will use ` console.log("Started server at port " + port + "."); }); {% endcodeblock %} +There is still one problem - the leak of "server.js" file. We can add a condition: +{% codeblock server.js lang:javascript %} + //Source code leakage mitigated + var http = require("http"); + var fs = require("fs"); + var mime = require("mime-types"); + var path = require("path"); + var port = 8080; + var server = http.createServer(function (req, res) { + var urlObject = new URL(req.url, "http://localhost"); + var filename = ""; + try { + filename = "." + decodeURIComponent(urlObject.pathname); + } catch(ex) { + //Malformed URI means bad request. + res.writeHead(400, "Bad Request", { + "Content-Type": "text/plain" + }); + res.end("400 Bad Request"); + return; + } + filename = filename.replace(/\\/g,"/").replace(/\/\.\.?(?=\/|$)/g,"/").replace(/\/+/g,"/"); //Poor mans URL sanitizer + if(filename == "./") filename = "./index.html"; + var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. + if(filename == "./" + path.basename(__filename)) { + //Prevent leakage of server source code + res.writeHead(403, "Forbidden", { + "Content-Type": "text/plain" + }); + res.end("403 Forbidden"); + return; + } + fs.readFile(filename, function(err, data) { + if(err) { + if(err.code == "ENOENT") { + //ENOENT means "File doesn't exist" + res.writeHead(404, "Not Found", { + "Content-Type": "text/plain" + }); + res.end("404 Not Found"); + } else { + res.writeHead(500, "Internal Server Error", { + "Content-Type": "text/plain" + }); + res.end("500 Internal Server Error! Reason: " + err.message); + } + } else { + res.writeHead(200, "OK", { + "Content-Type": mime.lookup(ext) || undefined + }); + res.end(data); + } + }); + }); + server.listen(port, function() { + console.log("Started server at port " + port + "."); + }); +{% endcodeblock %} We have now very simple HTTP static server, serving at *localhost:8080*. **Wait... Did we forget about [SVR.JS](https://svrjs.duckdns.org)?** SVR.JS is a web server running on Node.JS, that supports not only static file serving, but also directory listings, path rewriting, complete URL sanitation, HTTPS, HTTP/2.0, expandability via mods and server-side JavaScript, and it's configurable.