diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index db9d314..da49cac 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -17,6 +17,8 @@ jobs: env: REGISTRY: docker.io DOCKERHUB_USER_LC: ${{ secrets.DOCKERHUB_USERNAME }} + permissions: + contents: read steps: - name: Checkout diff --git a/backend/package-lock.json b/backend/package-lock.json index 65d2740..9684ea5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,6 +18,7 @@ "macaddress": "^0.5.3", "pino": "^9.6.0", "pino-pretty": "^13.0.0", + "qs": "^6.14.1", "whois-json": "^2.0.4" } }, @@ -855,6 +856,21 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1224,6 +1240,21 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -1950,11 +1981,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" diff --git a/backend/package.json b/backend/package.json index f14648e..09cc24e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "macaddress": "^0.5.3", "pino": "^9.6.0", "pino-pretty": "^13.0.0", + "qs": "^6.14.1", "whois-json": "^2.0.4" } -} \ No newline at end of file +} diff --git a/backend/routes/whoisLookup.js b/backend/routes/whoisLookup.js index 6d965c7..a88588b 100644 --- a/backend/routes/whoisLookup.js +++ b/backend/routes/whoisLookup.js @@ -5,7 +5,7 @@ const whois = require('whois-json'); const pino = require('pino'); // Import utilities -const { isValidIp, isValidDomain } = require('../utils'); +const { isValidIp, isValidDomain, isPrivateIp } = require('../utils'); // Logger for this module const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); @@ -26,6 +26,11 @@ router.get('/', async (req, res, next) => { return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' }); } + if (isValidIp(query) && isPrivateIp(query)) { + logger.warn({ requestIp, query }, 'Attempt to WHOIS lookup private IP blocked'); + return res.status(403).json({ success: false, error: 'WHOIS lookup for private or local IP addresses is not supported.' }); + } + // Note: No isPrivateIp check here, as WHOIS for IPs might be desired regardless of range, // and domain lookups don't involve IP ranges. diff --git a/backend/server.js b/backend/server.js index fe33dab..0ebf213 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,10 +5,10 @@ const Sentry = require("@sentry/node"); // Initialize Sentry BEFORE requiring any other modules! Sentry.init({ - // DSN should now be available from process.env if set in .env - dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456", - // Enable tracing - Adjust sample rate as needed - tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, + // DSN should now be available from process.env if set in .env + dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456", + // Enable tracing - Adjust sample rate as needed + tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, }); // DEBUG: Check Sentry object after init @@ -36,10 +36,10 @@ const macLookupRoutes = require('./routes/macLookup'); // --- Logger Initialisierung --- const logger = pino({ - level: process.env.LOG_LEVEL || 'info', - transport: process.env.NODE_ENV !== 'production' - ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } } - : undefined, + level: process.env.LOG_LEVEL || 'info', + transport: process.env.NODE_ENV !== 'production' + ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } } + : undefined, }); // Create Express app instance @@ -71,11 +71,11 @@ app.set('trust proxy', parseInt(process.env.TRUST_PROXY_COUNT || '2', 10)); // A // --- Rate Limiter --- // Apply a general limiter to most routes const generalLimiter = rateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP, ensure integer + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || (5 * 60 * 1000).toString(), 10), // Default 5 minutes + max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers - message: { success: false, error: 'Too many requests from this IP, please try again after 5 minutes' }, + message: { success: false, error: 'Too many requests from this IP, please try again after a while' }, keyGenerator: (req, res) => req.ip, // Use client IP address from Express handler: (req, res, next, options) => { logger.warn({ ip: req.ip, route: req.originalUrl }, 'Rate limit exceeded'); @@ -87,15 +87,8 @@ const generalLimiter = rateLimit({ } }); -// Apply the limiter to specific API routes that perform external actions -// Note: /api/ipinfo and /api/version are often excluded as they are less resource-intensive -app.use('/api/ping', generalLimiter); -app.use('/api/traceroute', generalLimiter); -app.use('/api/lookup', generalLimiter); -app.use('/api/dns-lookup', generalLimiter); -app.use('/api/whois-lookup', generalLimiter); -app.use('/api/port-scan', generalLimiter); -app.use('/api/mac-lookup', generalLimiter); +// Apply the limiter to ALL API routes +app.use('/api', generalLimiter); // --- API Routes --- @@ -116,12 +109,12 @@ app.use('/api/mac-lookup', macLookupRoutes); if (Sentry.Handlers && Sentry.Handlers.errorHandler) { app.use(Sentry.Handlers.errorHandler({ shouldHandleError(error) { - // Capture all 500 errors - if (error.status === 500) return true; - // Capture specific client errors if needed, e.g., 403 - // if (error.status === 403) return true; - // By default, capture only server errors (5xx) - return error.status >= 500; + // Capture all 500 errors + if (error.status === 500) return true; + // Capture specific client errors if needed, e.g., 403 + // if (error.status === 403) return true; + // By default, capture only server errors (5xx) + return error.status >= 500; }, })); } else { @@ -182,9 +175,9 @@ async function gracefulShutdown(signal) { await Sentry.close(2000); // 2 second timeout logger.info('Sentry closed.'); } catch (e) { - logger.error({ error: e.message }, 'Error closing Sentry'); + logger.error({ error: e.message }, 'Error closing Sentry'); } finally { - process.exit(128 + signals[signal]); // Standard exit code for signals + process.exit(128 + signals[signal]); // Standard exit code for signals } }); } else { @@ -194,9 +187,9 @@ async function gracefulShutdown(signal) { await Sentry.close(2000); logger.info('Sentry closed (server never started).'); } catch (e) { - logger.error({ error: e.message }, 'Error closing Sentry (server never started)'); + logger.error({ error: e.message }, 'Error closing Sentry (server never started)'); } finally { - process.exit(128 + signals[signal]); + process.exit(128 + signals[signal]); } } @@ -209,5 +202,5 @@ async function gracefulShutdown(signal) { // Register signal handlers Object.keys(signals).forEach((signal) => { - process.on(signal, () => gracefulShutdown(signal)); + process.on(signal, () => gracefulShutdown(signal)); }); \ No newline at end of file