Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
184 changes: 0 additions & 184 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down