Comprehensive Server Security Hardening for Self-Hosted TVx
Date: October 21, 2025 Branch: dependabot-combined-test Issue: Server.js lacked production-grade security hardening for self-hosted deployment
Problem Description
The TVx Node.js server (server.js) was initially implemented with basic functionality but lacked essential security hardening required for self-hosted deployments. While suitable for development, the server was vulnerable to common web attacks and resource exhaustion in production environments.
Security Risks Identified:
No HTTP method validation (accepting any verb)
No URL length limits (vulnerable to buffer overflows)
No header size limits (vulnerable to header attacks)
No path traversal protection beyond basic checks
No Content-Type validation for POST endpoints
No file extension whitelisting
No range request validation for video files
No connection limits
Server fingerprinting exposure
No batch size limits for log processing
Missing security headers
No request timeout protection
Race conditions in client-side logging
Missing OPTIONS preflight handling
Root Cause Analysis
Step 1: Security Assessment
The server was built with a “works for development” mentality but lacked production security layers. Key vulnerabilities:
HTTP Method Tampering: Server accepted any HTTP method (PUT, DELETE, TRACE, etc.)
constALLOWED_METHODS=newSet(['GET','HEAD','POST','OPTIONS']);if(!ALLOWED_METHODS.has(req.method)){res.writeHead(405,{'Content-Type':'text/plain','Allow':Array.from(ALLOWED_METHODS).join(', ')});res.end('Method Not Allowed');return;}// Fast path for OPTIONS (preflight)if(req.method==='OPTIONS'){res.writeHead(204,{'Allow':Array.from(ALLOWED_METHODS).join(', '),'Access-Control-Allow-Methods':'GET, HEAD, POST, OPTIONS','Access-Control-Allow-Headers':'content-type','Access-Control-Max-Age':'600'});res.end();return;}
2. Header Size Enforcement
1
2
3
4
5
6
7
8
9
10
constMAX_HEADER_SIZE=8192;// 8KB max for headers// Enforce max header size (protect against oversized header attacks)constheaderSize=Buffer.byteLength((req.rawHeaders||[]).join(''),'utf8');if(headerSize>MAX_HEADER_SIZE){res.writeHead(431,{'Content-Type':'text/plain'});res.end('Request Header Fields Too Large');writeLog(`[${newDate().toISOString()}] WARN: Headers too large from ${clientIp} (${headerSize} bytes)`);return;}
constMAX_URL_LENGTH=2048;constSUSPICIOUS_PATTERNS=[/\.\.\//g,// Directory traversal/\.\.\\/g,// Windows path traversal/%2e%2e%2f/gi,// URL encoded traversal/%252e%252e%252f/gi,// Double URL encoded/\/\//g,// Double slashes];if(req.url.length>MAX_URL_LENGTH){res.writeHead(414);res.end('URI Too Long');return;}for(constpatternofSUSPICIOUS_PATTERNS){if(pattern.test(req.url.toLowerCase())){res.writeHead(400);res.end('Bad Request');return;}}
4. Enhanced Content-Type Validation for POST Endpoints
1
2
3
4
5
6
7
8
9
10
11
12
if(req.method==='POST'&&pathname==='/log'){// Validate Content-Type header (support sendBeacon string payloads)constcontentType=req.headers['content-type']||'';constisJson=contentType.includes('application/json');constisBeaconText=contentType.startsWith('text/plain');// sendBeacon default for stringsif(!isJson&&!isBeaconText){res.writeHead(415,{'Content-Type':'text/plain'});res.end('Unsupported Media Type - Expected application/json');return;}// ... rest of log handling}
5. Simplified Body Accumulation
1
2
3
4
5
6
7
8
9
10
11
12
13
req.on('data',chunk=>{bodySize+=chunk.length;// Prevent large payload attacksif(bodySize>MAX_BODY_SIZE){res.writeHead(413,{'Content-Type':'text/plain'});res.end('Payload Too Large');req.destroy();return;}body+=chunk;// Simplified - no unnecessary slice math});
6. File Extension Whitelisting
1
2
3
4
5
6
7
8
9
10
11
constallowedExtensions=newSet(['.html','.css','.js','.json','.png','.jpg','.jpeg','.ico','.mp4','.webmanifest','.txt','.svg','.woff','.woff2','.ttf','.eot']);if(!allowedExtensions.has(ext)){res.writeHead(403);res.end('Forbidden - File type not allowed');return;}
7. Enhanced Range Request Validation with Content-Range
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(ext==='.mp4'&&req.headers.range){constrange=req.headers.range;constparts=range.replace(/bytes=/,'').split('-');conststart=parseInt(parts[0],10);constend=parts[1]?parseInt(parts[1],10):stats.size-1;// Validate range valuesif(isNaN(start)||isNaN(end)||start<0||end>=stats.size||start>end){res.writeHead(416,{'Content-Type':'text/plain','Content-Range':`bytes */${stats.size}`});res.end('Range Not Satisfiable');return;}// ... rest of range handling}
constMAX_BODY_SIZE=1048576;// 1MB max for POST bodiesconstREQUEST_TIMEOUT=30000;// 30 secondsserver.maxConnections=100;// Max concurrent connectionsserver.keepAliveTimeout=65000;// Keep-alive timeoutserver.headersTimeout=66000;// Headers timeoutreq.setTimeout(REQUEST_TIMEOUT,()=>{res.writeHead(408);res.end('Request Timeout');});
10. Batch Processing Limits
1
2
3
4
5
if(Array.isArray(logData.logs)){constmaxBatchSize=100;constlogsToProcess=logData.logs.slice(0,maxBatchSize);// Process limited batch}
// Clean up old entries every 5 minutes to prevent memory bloatsetInterval(()=>{constnow=Date.now();for(const[key,data]ofrateLimitStore.entries()){if(now-data.resetTime>300000){// 5 minutesrateLimitStore.delete(key);}}},300000).unref();// Don't hold event loop open
14. Enhanced Error Handling and Logging
1
2
3
4
5
6
7
// Security events logged with contextwriteLog(`[${newDate().toISOString()}] SECURITY: Suspicious URL pattern detected from ${clientIp}: ${req.url}`);// Request errors handled gracefullyreq.on('error',(err)=>{writeLog(`[${newDate().toISOString()}] ERROR: Request error on /log - ${err.message}`);});
15. Client-Side Logger Simplified for Real-Time Visibility
// Immediate logging for all levels - simplified for home debuggingconstsendLog=async(level:string,message:string)=>{try{awaitfetch('/log',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({level,message}),});}catch(error){// Silent fail to avoid log loops}};exportconstlogger={log:(message:string)=>{console.log(message);sendLog('info',message);},error:(message:string)=>{console.error(message);sendLog('error',message);},warn:(message:string)=>{console.warn(message);sendLog('warn',message);},info:(message:string)=>{console.info(message);sendLog('info',message);},};
Design Decision: After implementing batching and selective logging (errors/warnings only), we reverted to the original immediate-send approach for all log levels to maintain real-time visibility into application events. For home use, seeing toast notifications, channel changes, and stream URLs in real-time via docker logs is more valuable than optimizing for log volume reduction.
Testing Commands
1. Build Hardened Docker Image
1
2
3
4
5
cd /Users/ed/TVx
docker build -t tvx:test-hardened \--build-argVITE_M3U_URL=http://192.168.22.2:8000/api/channels.m3u \--build-argVITE_XMLTV_URL=http://192.168.22.2:8000/api/xmltv.xml \.
# Test HTTP method validation
curl -X PUT -s-o /dev/null -w"PUT: %{http_code}\n" http://localhost:8777/
curl -X DELETE -s-o /dev/null -w"DELETE: %{http_code}\n" http://localhost:8777/
# Test Content-Type validation
curl -X POST -H"Content-Type: text/plain"-d"test"\-s-o /dev/null -w"Wrong CT: %{http_code}\n" http://localhost:8777/log
curl -X POST -H"Content-Type: application/json"-d'{"level":"info","message":"test"}'\-s-o /dev/null -w"Correct CT: %{http_code}\n" http://localhost:8777/log
# Test path traversal protection
curl -s-o /dev/null -w"Traversal: %{http_code}\n"\"http://localhost:8777/../../../etc/passwd"# Test range request validation
curl -H"Range: bytes=invalid"-s-o /dev/null -w"Bad Range: %{http_code}\n"\
http://localhost:8777/loading-VHS.mp4
4. Performance Testing
1
2
3
4
5
6
# Test video streaming still works
curl -I-H"Range: bytes=0-1023" http://localhost:8777/loading-VHS.mp4
# Test normal page load
curl -s-o /dev/null -w"Page load: %{http_code} (%{time_total}s)\n"\
http://localhost:8777/
Server running on port 80
Process ID: 1
Node version: v20.19.5
Max connections: 100
Static directory: /usr/share/nginx/html
[2025-10-21T22:06:53.161Z] WARN: Method PUT not allowed from ::ffff:192.168.65.1
[2025-10-21T22:06:53.162Z] SECURITY: Suspicious URL pattern detected from ::ffff:192.168.65.1: /../../../etc/passwd
Security is layered - Multiple validation points prevent single-point failures
Performance doesn’t require sacrifice - Security can be lightweight and efficient
Home servers need production hardening - Even trusted networks have risks
Testing is essential - Manual security testing caught issues automated tools might miss
Logging is security - Detailed security event logging enables monitoring and forensics
Future Security Enhancements
Could Add (If Needed)
Rate limiting per IP - Already implemented, can be tuned
Request signing - For high-security environments
CSP headers - Content Security Policy for enhanced XSS protection
HSTS headers - HTTP Strict Transport Security (requires HTTPS)
API key authentication - For multi-user scenarios
Log encryption - For sensitive environments
Automated security scanning - Integration with tools like Trivy
Not Needed for Home Use
❌ Full authentication system (trusted network)
❌ Database encryption (no sensitive data stored)
❌ Advanced intrusion detection (simple home setup)
❌ Multi-factor authentication (single admin)
❌ Audit trails beyond logging (sufficient for home)
Deployment Recommendations
For Home Use (Current Setup)
✅ Container with resource limits
✅ Non-root user execution
✅ Security headers enabled
✅ Comprehensive validation
✅ Enhanced logging
For Production Use (Additional)
🔒 HTTPS/TLS termination
🔒 Reverse proxy (nginx/Traefik)
🔒 Network segmentation
🔒 Regular security updates
🔒 Automated vulnerability scanning
Conclusion
The TVx server is now production-hardened with comprehensive security measures while maintaining the simplicity and performance required for home IPTV streaming. All security layers are lightweight, efficient, and tuned for family use without compromising the vintage TV experience.