From 567bd2941787b6fb30f740db5b1511e1c529e350 Mon Sep 17 00:00:00 2001 From: Jeff Caldwell Date: Fri, 1 Aug 2025 19:35:14 -0400 Subject: [PATCH] add rate limiting protection --- .env | 2 +- .env.example | 6 +++++ CHANGELOG.md | 26 ++++++++++++++++++-- README.md | 12 ++++++++++ TESTING.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 3 ++- server.js | 57 +++++++++++++++++++++++++++++++++++++++---- 7 files changed, 163 insertions(+), 11 deletions(-) diff --git a/.env b/.env index 5cd2948..00a837c 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Authentication test configuration -ENABLE_AUTH=true +ENABLE_AUTH=false AUTH_USERNAME=admin AUTH_PASSWORD=admin SESSION_SECRET=test-secret-key-for-development diff --git a/.env.example b/.env.example index acc6d78..74590d3 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,12 @@ NODE_ENV=development # Environment mode (development/production) CERT_DIR= # Custom certificate storage directory (optional) DEFAULT_THEME=dark # Default theme mode (dark/light) +# Rate Limiting Configuration +CLI_RATE_LIMIT_WINDOW=900000 # CLI rate limit window in ms (default: 15 minutes) +CLI_RATE_LIMIT_MAX=10 # Max CLI operations per window (default: 10) +API_RATE_LIMIT_WINDOW=900000 # API rate limit window in ms (default: 15 minutes) +API_RATE_LIMIT_MAX=100 # Max API requests per window (default: 100) + # Authentication Configuration ENABLE_AUTH=false # Enable user authentication (true/false) AUTH_USERNAME=admin # Username for authentication (when ENABLE_AUTH=true) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f11f4..81337d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.4.0] - 2025-08-01 +## [1.4.1] - 2025-08-01 + +### Added +- **Rate Limiting Protection**: Comprehensive rate limiting to prevent CLI command abuse + - Separate rate limiters for CLI operations (certificate generation, CA management) and API requests + - Configurable rate limits with environment variables (CLI: 10 ops/15min, API: 100 req/15min) + - Per-user and per-IP rate limiting for authenticated and anonymous users + - Protection against automated attacks and resource exhaustion +- **Rate Limiting Testing**: Comprehensive testing procedures and automated test script +- **Environment Configuration**: Added rate limiting configuration options to .env.example + +### Security +- **Rate Limiting Protection**: Comprehensive protection against CLI command abuse and automated attacks +- **Resource Protection**: Prevents excessive CLI operations that could impact server performance +- **Multi-layer Security**: Combined IP-based and user-based rate limiting for enhanced protection + +### Technical +- Added `express-rate-limit@^7.4.0` dependency for robust rate limiting functionality +- Enhanced server middleware with configurable rate limiting for different endpoint types +- Automated test script for validating rate limiting functionality + +## [1.4.0] ### Added - **OpenID Connect (OIDC) SSO Authentication**: Full OpenID Connect integration for single sign-on support @@ -175,7 +196,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **v1.2.0**: Complete Docker containerization support - **v1.3.0**: PFX generation, improved UI/UX, and enhanced certificate management - **v1.4.0**: OpenID Connect SSO authentication and enhanced Root CA management -- **Current**: Full-featured mkcert Web UI with comprehensive certificate format support and enterprise SSO +- **v1.4.1**: Rate limiting protection and security enhancements +- **Current**: Full-featured mkcert Web UI with comprehensive certificate format support, enterprise SSO, and rate limiting protection ## Contributing diff --git a/README.md b/README.md index e56d00d..55bf70a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A modern web interface for managing SSL certificates using the mkcert CLI tool. - **📋 Multiple Formats**: Generate PEM, CRT, and PFX (PKCS#12) certificates on-demand - **🔒 Flexible Authentication**: Basic authentication and enterprise SSO with OpenID Connect (OIDC) - **🏢 Enterprise SSO**: OpenID Connect integration for Azure AD, Google, and other OIDC providers +- **🛡️ Rate Limiting**: Built-in protection against CLI command abuse and automated attacks - **🌐 HTTPS Support**: Auto-generated SSL certificates for secure web interface - **📋 Certificate Management**: View, download, archive, and restore certificates - **🎨 Modern UI**: Dark/light themes with responsive design and mobile support @@ -118,6 +119,12 @@ FORCE_HTTPS=true # Redirect HTTP to HTTPS # Certificate Settings CERTIFICATE_FORMAT=pem # Default format: 'pem' or 'crt' + +# Rate Limiting Configuration +CLI_RATE_LIMIT_WINDOW=900000 # CLI operations window in ms (default: 15 minutes) +CLI_RATE_LIMIT_MAX=10 # Max CLI operations per window (default: 10) +API_RATE_LIMIT_WINDOW=900000 # API requests window in ms (default: 15 minutes) +API_RATE_LIMIT_MAX=100 # Max API requests per window (default: 100) ``` ### Authentication Setup @@ -347,6 +354,11 @@ mkcertWeb/ - **Development Focus**: Designed for local development environments - **Flexible Authentication**: Basic authentication and enterprise SSO with OpenID Connect - **Enterprise SSO**: Secure OIDC integration with proper token validation and session management +- **Rate Limiting Protection**: Built-in protection against CLI command abuse and automated attacks + - **CLI Operations**: Limited to 10 operations per 15-minute window (certificate generation, CA management) + - **API Requests**: Limited to 100 requests per 15-minute window (general API endpoints) + - **Per-User Limiting**: Rate limits applied per IP address and authenticated user + - **Configurable Limits**: All rate limits can be adjusted via environment variables - **Regular User Execution**: Runs without root privileges (except for `mkcert -install`) - **Read-Only Protection**: Root directory certificates cannot be deleted - **Session Security**: HTTP-only cookies with CSRF protection and secure OIDC flows diff --git a/TESTING.md b/TESTING.md index ecbd47b..2a38a58 100644 --- a/TESTING.md +++ b/TESTING.md @@ -729,7 +729,71 @@ wget --load-cookies=/tmp/auth-cookies.txt \ # Should either reject or sanitize the input ``` -### 3. File Access Security Testing +### 3. Rate Limiting Security Testing +```bash +# Test CLI rate limiting for certificate generation +echo "Testing CLI rate limiting..." + +# Login first to get valid session +wget --post-data='{"username":"admin","password":"admin"}' \ + --header='Content-Type: application/json' \ + --save-cookies=/tmp/rate-limit-cookies.txt \ + http://localhost:3000/api/auth/login \ + -O /tmp/temp.json + +# Test rapid certificate generation (should hit rate limit) +echo "Attempting rapid certificate generation to test rate limiting..." +for i in {1..12}; do + echo "Request $i:" + wget --load-cookies=/tmp/rate-limit-cookies.txt \ + --post-data="{\"domains\":[\"test$i.local\"],\"format\":\"pem\"}" \ + --header='Content-Type: application/json' \ + http://localhost:3000/api/generate \ + -O /tmp/rate-limit-test-$i.json 2>&1 + + if grep -q "429\|Too many" /tmp/rate-limit-test-$i.json; then + echo "✓ Rate limit triggered at request $i" + break + elif [ $i -gt 10 ]; then + echo "⚠ Rate limit may not be working (completed $i requests)" + fi + sleep 1 +done + +# Test API rate limiting for general endpoints +echo "Testing API rate limiting..." +for i in {1..25}; do + wget --load-cookies=/tmp/rate-limit-cookies.txt \ + -qO- http://localhost:3000/api/certificates > /tmp/api-rate-$i.json 2>&1 + + if grep -q "429\|Too many" /tmp/api-rate-$i.json; then + echo "✓ API rate limit working (triggered at request $i)" + break + fi + + if [ $((i % 10)) -eq 0 ]; then + echo "Completed $i API requests..." + fi +done + +# Test rate limit headers +echo "Testing rate limit headers..." +wget --load-cookies=/tmp/rate-limit-cookies.txt \ + --server-response \ + http://localhost:3000/api/status \ + -O /tmp/rate-headers.txt 2>&1 + +grep -E "X-RateLimit|RateLimit" /tmp/rate-headers.txt && echo "✓ Rate limit headers present" + +# Test rate limiting with different IPs (if possible) +# Note: This is limited in single-machine testing +echo "✓ Rate limiting tests completed" + +# Clean up rate limiting test files +rm -f /tmp/rate-limit-*.json /tmp/api-rate-*.json /tmp/rate-headers.txt +``` + +### 4. File Access Security Testing ```bash # Try to access files outside certificate directory (should fail) wget --load-cookies=/tmp/auth-cookies.txt \ @@ -745,7 +809,7 @@ wget http://localhost:3000/api/download/rootca \ grep -q "401" /tmp/unauth-download.txt && echo "✓ File download requires authentication" ``` -### 4. Session Management Security Testing +### 5. Session Management Security Testing ```bash # Test concurrent sessions # Login from multiple "clients" diff --git a/package.json b/package.json index e10bcc2..9708da3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mkcert-web-ui", - "version": "1.4.0", + "version": "1.4.1", "description": "Web UI middleware for managing mkcert CLI and certificate files", "main": "server.js", "scripts": { @@ -35,6 +35,7 @@ "cors": "^2.8.5", "dotenv": "^16.6.1", "express": "^4.19.2", + "express-rate-limit": "^7.4.0", "express-session": "^1.17.3", "fs-extra": "^11.2.0", "passport": "^0.7.0", diff --git a/server.js b/server.js index 3bca1d4..eb76d80 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ const session = require('express-session'); const bcrypt = require('bcryptjs'); const passport = require('passport'); const OpenIDConnectStrategy = require('passport-openidconnect'); +const rateLimit = require('express-rate-limit'); const app = express(); const PORT = process.env.PORT || 3000; @@ -36,6 +37,46 @@ const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET; const OIDC_CALLBACK_URL = process.env.OIDC_CALLBACK_URL || `http://localhost:${PORT}/auth/oidc/callback`; const OIDC_SCOPE = process.env.OIDC_SCOPE || 'openid profile email'; +// Rate limiting configuration +const CLI_RATE_LIMIT_WINDOW = parseInt(process.env.CLI_RATE_LIMIT_WINDOW) || 15 * 60 * 1000; // 15 minutes +const CLI_RATE_LIMIT_MAX = parseInt(process.env.CLI_RATE_LIMIT_MAX) || 10; // 10 requests per window +const API_RATE_LIMIT_WINDOW = parseInt(process.env.API_RATE_LIMIT_WINDOW) || 15 * 60 * 1000; // 15 minutes +const API_RATE_LIMIT_MAX = parseInt(process.env.API_RATE_LIMIT_MAX) || 100; // 100 requests per window + +// Create rate limiters +const cliRateLimiter = rateLimit({ + windowMs: CLI_RATE_LIMIT_WINDOW, + max: CLI_RATE_LIMIT_MAX, + message: { + error: 'Too many CLI operations, please try again later.', + retryAfter: Math.ceil(CLI_RATE_LIMIT_WINDOW / 1000) + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + // Rate limit by IP address and user (if authenticated) + const ip = req.ip || req.connection.remoteAddress; + const user = req.user?.username || req.session?.username || 'anonymous'; + return `cli:${ip}:${user}`; + } +}); + +const apiRateLimiter = rateLimit({ + windowMs: API_RATE_LIMIT_WINDOW, + max: API_RATE_LIMIT_MAX, + message: { + error: 'Too many API requests, please try again later.', + retryAfter: Math.ceil(API_RATE_LIMIT_WINDOW / 1000) + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + const ip = req.ip || req.connection.remoteAddress; + const user = req.user?.username || req.session?.username || 'anonymous'; + return `api:${ip}:${user}`; + } +}); + // Middleware app.use(cors()); app.use(bodyParser.json()); @@ -295,8 +336,14 @@ const executeCommand = (command) => { // Routes +// Apply general API rate limiting to all API routes (except auth endpoints) +app.use('/api/certificates', apiRateLimiter); +app.use('/api/download', apiRateLimiter); +app.use('/api/rootca', apiRateLimiter); +app.use('/api/config', apiRateLimiter); + // Get mkcert status and CA info -app.get('/api/status', requireAuth, async (req, res) => { +app.get('/api/status', requireAuth, cliRateLimiter, async (req, res) => { try { const result = await executeCommand('mkcert -CAROOT'); const caRoot = result.stdout.trim(); @@ -360,7 +407,7 @@ app.get('/api/status', requireAuth, async (req, res) => { }); // Install CA (mkcert -install) -app.post('/api/install-ca', requireAuth, async (req, res) => { +app.post('/api/install-ca', requireAuth, cliRateLimiter, async (req, res) => { try { const result = await executeCommand('mkcert -install'); res.json({ @@ -378,7 +425,7 @@ app.post('/api/install-ca', requireAuth, async (req, res) => { }); // Generate new Root CA (mkcert -install creates a new CA if one doesn't exist) -app.post('/api/generate-ca', requireAuth, async (req, res) => { +app.post('/api/generate-ca', requireAuth, cliRateLimiter, async (req, res) => { try { // First check if mkcert is available try { @@ -598,7 +645,7 @@ app.get('/api/rootca/info', requireAuth, async (req, res) => { }); // Generate certificate -app.post('/api/generate', requireAuth, async (req, res) => { +app.post('/api/generate', requireAuth, cliRateLimiter, async (req, res) => { try { const { domains, format = 'pem' } = req.body; @@ -1019,7 +1066,7 @@ app.get('/api/download/bundle/:certname', requireAuth, (req, res) => { }); // Generate PFX file from certificate and key -app.post('/api/generate/pfx/*', requireAuth, async (req, res) => { +app.post('/api/generate/pfx/*', requireAuth, cliRateLimiter, async (req, res) => { try { // Parse the wildcard path to extract folder and certname const fullPath = req.params[0]; // Get the wildcard part