diff --git a/self-hosting-example/package.json b/self-hosting-example/package.json index 6f4c4bb..c31ae59 100644 --- a/self-hosting-example/package.json +++ b/self-hosting-example/package.json @@ -12,7 +12,8 @@ "author": "", "license": "MIT", "dependencies": { - "express": "^4.16.2" + "express": "^4.16.2", + "express-rate-limit": "^7.1.5" }, "devDependencies": { "node-fetch": "2.6.7", diff --git a/self-hosting-example/scripts/download-assets.js b/self-hosting-example/scripts/download-assets.js index c26f90a..c602e05 100644 --- a/self-hosting-example/scripts/download-assets.js +++ b/self-hosting-example/scripts/download-assets.js @@ -35,6 +35,26 @@ const isRuntimeVersion = (value) => { return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value); } +// Sanitize input to prevent path traversal attacks +const sanitizePathInput = (input) => { + if (!input || typeof input !== 'string') { + throw new Error('Invalid input: must be a non-empty string'); + } + + // Remove any path traversal sequences and normalize the path + const sanitized = input + .replace(/\.\./g, '') // Remove .. sequences + .replace(/[\/\\]/g, '') // Remove path separators + .replace(/[^a-zA-Z0-9\-_]/g, '') // Only allow alphanumeric, hyphens, and underscores + .trim(); + + if (!sanitized) { + throw new Error('Invalid input: contains only invalid characters'); + } + + return sanitized; +} + const downloadRuntime = async(versions, updateManifest) => { const runtimeFolder = path.join( __dirname, '..', 'public', 'runtime'); fs.mkdirSync(runtimeFolder, { recursive: true }); @@ -47,7 +67,8 @@ const downloadRuntime = async(versions, updateManifest) => { if (isRuntimeVersion(mappedVersion)) { numericVersion = mappedVersion; console.log(`release channel ${version} of Runtime: ${numericVersion}`); - const channelPath = path.join(runtimeFolder, version); + const sanitizedVersion = sanitizePathInput(version); + const channelPath = path.join(runtimeFolder, sanitizedVersion); fs.writeFileSync(channelPath, numericVersion); } } else { @@ -59,7 +80,8 @@ const downloadRuntime = async(versions, updateManifest) => { if (numericVersion) { console.log(`downloading version ${numericVersion} of Runtime`); - const runtimePath = path.join(runtimeFolder, numericVersion); + const sanitizedNumericVersion = sanitizePathInput(numericVersion); + const runtimePath = path.join(runtimeFolder, sanitizedNumericVersion); if (downloadAsset(runtimePath, `${RUNTIME_BASE_URL}/${numericVersion}`)) { const versionListPath = path.join('./public', 'runtimeVersions'); fs.writeFileSync(versionListPath, numericVersion, { flag: 'a+' } ); @@ -69,7 +91,7 @@ const downloadRuntime = async(versions, updateManifest) => { } const runtimeFolder64 = path.join( __dirname, '..', 'public', 'runtime', 'x64'); fs.mkdirSync(runtimeFolder64, { recursive: true }); - downloadAsset(path.join(runtimeFolder64, numericVersion), `${RUNTIME_BASE_URL}/x64/${numericVersion}`) + downloadAsset(path.join(runtimeFolder64, sanitizedNumericVersion), `${RUNTIME_BASE_URL}/x64/${numericVersion}`) } } } diff --git a/self-hosting-example/server.js b/self-hosting-example/server.js index 9565215..f9fbfa3 100644 --- a/self-hosting-example/server.js +++ b/self-hosting-example/server.js @@ -1,20 +1,33 @@ const express = require('express'); +const rateLimit = require('express-rate-limit'); const app = express(); const path = require('path'); const port = process.env.PORT || 5555; +// Disable X-Powered-By header to hide Express framework information +app.disable('x-powered-by'); + +// Rate limiting configuration to prevent DoS attacks +const fileEndpointLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}); + app.use(express.static('public')); -// For the demo -app.get('/manifest', (req, res) => { +// For the demo - with rate limiting to prevent DoS attacks +app.get('/manifest', fileEndpointLimiter, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'app.json')); }); -// For the demo -app.get('/app', (req, res) => { +// For the demo - with rate limiting to prevent DoS attacks +app.get('/app', fileEndpointLimiter, (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); -app.get('/launch', (req, res) => { +app.get('/launch', fileEndpointLimiter, (req, res) => { res.sendFile(path.join(__dirname, 'launch.html')); })