diff --git a/README.md b/README.md index 2edf6c2..089d635 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,33 @@ sudo docker compose up -d The API front-end is available at [http://localhost:49155](http://localhost:49155). +## Security Considerations + +**SSRF (Server-Side Request Forgery) Risk:** This service accepts user-provided URLs via the `server` parameter and makes HTTP requests to those URLs. This implementation does not include SSRF protection for simplicity in internal trusted network deployments. + +### Potential Attack Vectors + +If this service is exposed to untrusted users or networks, attackers could potentially: + +- **Access cloud metadata services** (e.g., AWS at 169.254.169.254) to steal credentials +- **Scan internal networks** to discover internal services and infrastructure +- **Access internal services** that are not internet-facing +- **Bypass firewall rules** by using this service as a proxy + +### Recommendations + +1. **Deploy only on trusted internal networks** - Do not expose this service to the public internet +2. **Implement network segmentation** - Limit what networks this service can reach +3. **Add authentication** - Require authentication for API access if needed +4. **Monitor for abuse** - Log and review access patterns +5. **Security audit** - Conduct a security audit before production deployment + +For production deployments requiring public access, consider implementing: +- Private network IP blocking (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x, 169.254.x.x) +- DNS resolution validation +- Request rate limiting +- URL allowlisting + ## Method of Operation The delay-Doppler data is computed as follows: diff --git a/src/server.js b/src/server.js index 695bb3d..0b8aac9 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,5 @@ import express from 'express'; import cors from 'cors'; -import dns from 'dns'; -import { promisify } from 'util'; import {checkTar1090, getTar1090} from './node/tar1090.js'; import {checkAdsbLol, getAdsbLol} from './node/adsblol.js'; @@ -11,9 +9,6 @@ import {calculateDopplerFromVelocity, calculateWavelength} from './node/doppler. import {SyntheticRNG, parseSyntheticConfig, validateSyntheticConfig, generateSyntheticFrame, convertToFrameFormat} from './node/synthetic.js'; -const resolve4 = promisify(dns.resolve4); -const resolve6 = promisify(dns.resolve6); - const app = express(); app.use(cors()); const port = process.env.PORT || 49155; @@ -30,46 +25,6 @@ const adsbLolRadius = 40; app.use(express.static('public')); -/// @brief Check if an IP address is in a private or reserved range -/// @param ip IP address string (IPv4 or IPv6) -/// @return True if IP is private/reserved -function isPrivateIP(ip) { - const ipv4PrivateRanges = [ - /^127\./, - /^10\./, - /^172\.(1[6-9]|2[0-9]|3[0-1])\./, - /^192\.168\./, - /^169\.254\./, - /^0\.0\.0\.0$/, - ]; - - const ipv6PrivateRanges = [ - /^::1$/, - /^fe80:/i, - /^fc00:/i, - /^fd00:/i, - /^ff00:/i, - /^::ffff:/i, - ]; - - if (ipv4PrivateRanges.some(range => range.test(ip))) { - return true; - } - - if (ipv6PrivateRanges.some(range => range.test(ip))) { - return true; - } - - if (/^::ffff:/i.test(ip)) { - const ipv4Part = ip.replace(/^::ffff:/i, ''); - if (ipv4PrivateRanges.some(range => range.test(ipv4Part))) { - return true; - } - } - - return false; -} - app.get('/api/dd', async (req, res) => { if (req.originalUrl in dict) { @@ -110,75 +65,6 @@ app.get('/api/dd', async (req, res) => { } } - if (!isAdsbLol) { - const hostname = serverUrl.hostname; - - const privateIPv4Ranges = [ - /^127\./, - /^10\./, - /^172\.(1[6-9]|2[0-9]|3[0-1])\./, - /^192\.168\./, - /^169\.254\./, - /^0\.0\.0\.0$/, - /localhost/i - ]; - - const privateIPv6Ranges = [ - /^::1$/, - /^::$/, - /^fe80:/i, - /^fc00:/i, - /^fd00:/i, - /^ff00:/i, - ]; - - if (/^::ffff:/i.test(hostname)) { - const ipv4Part = hostname.replace(/^::ffff:/i, ''); - if (privateIPv4Ranges.some(range => range.test(ipv4Part))) { - return res.status(400).json({ error: 'Server URL points to private network' }); - } - } - - if (privateIPv4Ranges.some(range => range.test(hostname))) { - return res.status(400).json({ error: 'Server URL points to private network' }); - } - - if (privateIPv6Ranges.some(range => range.test(hostname))) { - return res.status(400).json({ error: 'Server URL points to private network' }); - } - - if (/^(0x[0-9a-f]+|\d+|0[0-7]+)$/i.test(hostname)) { - return res.status(400).json({ error: 'Server URL uses invalid IP format' }); - } - - if (!/^[\d.:]+$/.test(hostname)) { - try { - const resolutions = await Promise.allSettled([ - resolve4(hostname), - resolve6(hostname) - ]); - - const resolvedIPs = []; - for (const result of resolutions) { - if (result.status === 'fulfilled' && Array.isArray(result.value)) { - resolvedIPs.push(...result.value); - } - } - - if (resolvedIPs.length === 0) { - return res.status(400).json({ error: 'Unable to resolve server hostname' }); - } - - for (const ip of resolvedIPs) { - if (isPrivateIP(ip)) { - return res.status(400).json({ error: 'Server hostname resolves to private network' }); - } - } - } catch (error) { - return res.status(400).json({ error: 'Unable to resolve server hostname' }); - } - } - } let isServerValid; let midLat, midLon; @@ -280,76 +166,6 @@ app.get('/api/synthetic-detections', async (req, res) => { } } - if (!isAdsbLol) { - const hostname = serverUrl.hostname; - - const privateIPv4Ranges = [ - /^127\./, - /^10\./, - /^172\.(1[6-9]|2[0-9]|3[0-1])\./, - /^192\.168\./, - /^169\.254\./, - /^0\.0\.0\.0$/, - /localhost/i - ]; - - const privateIPv6Ranges = [ - /^::1$/, - /^::$/, - /^fe80:/i, - /^fc00:/i, - /^fd00:/i, - /^ff00:/i, - ]; - - if (/^::ffff:/i.test(hostname)) { - const ipv4Part = hostname.replace(/^::ffff:/i, ''); - if (privateIPv4Ranges.some(range => range.test(ipv4Part))) { - return res.status(400).json({ error: 'Server URL points to private network' }); - } - } - - if (privateIPv4Ranges.some(range => range.test(hostname))) { - return res.status(400).json({ error: 'Server URL points to private network' }); - } - - if (privateIPv6Ranges.some(range => range.test(hostname))) { - return res.status(400).json({ error: 'Server URL points to private network' }); - } - - if (/^(0x[0-9a-f]+|\d+|0[0-7]+)$/i.test(hostname)) { - return res.status(400).json({ error: 'Server URL uses invalid IP format' }); - } - - if (!/^[\d.:]+$/.test(hostname)) { - try { - const resolutions = await Promise.allSettled([ - resolve4(hostname), - resolve6(hostname) - ]); - - const resolvedIPs = []; - for (const result of resolutions) { - if (result.status === 'fulfilled' && Array.isArray(result.value)) { - resolvedIPs.push(...result.value); - } - } - - if (resolvedIPs.length === 0) { - return res.status(400).json({ error: 'Unable to resolve server hostname' }); - } - - for (const ip of resolvedIPs) { - if (isPrivateIP(ip)) { - return res.status(400).json({ error: 'Server hostname resolves to private network' }); - } - } - } catch (error) { - return res.status(400).json({ error: 'Unable to resolve server hostname' }); - } - } - } - // Initialize RNG const rng = new SyntheticRNG(syntheticConfig.seed);