HTTP Response Splitting and Header Injection

Medium Risk Input Validation
http-headersresponse-splittingheader-injectioncache-poisoningxsscrlf-injection

What it is

HTTP response splitting occurs when untrusted user input is used to construct HTTP response headers without proper validation or encoding. Attackers can inject carriage return (CR) and line feed (LF) characters to manipulate HTTP responses, inject additional headers, perform cache poisoning, cross-site scripting (XSS), or conduct session hijacking attacks.

const express = require('express');
const app = express();

// VULNERABLE: Direct user input in redirect header
app.get('/redirect', (req, res) => {
    const url = req.query.url;
    res.setHeader('Location', url);  // No validation
    res.status(302).send();
});

// VULNERABLE: User input in custom headers
app.get('/api/track', (req, res) => {
    const trackingId = req.query.tracking_id;
    const source = req.query.source;
    
    // Dangerous: Direct inclusion in headers
    res.setHeader('X-Tracking-ID', trackingId);
    res.setHeader('X-Source', source);
    res.json({ message: 'Tracking recorded' });
});

// VULNERABLE: Cookie setting with user data
app.post('/set-theme', (req, res) => {
    const theme = req.body.theme;
    const preferences = req.body.preferences;
    
    // No validation before setting cookies
    res.cookie('theme', theme);
    res.cookie('preferences', preferences);
    res.send('Theme updated');
});

// Malicious requests:
// GET /redirect?url=http://evil.com%0D%0ASet-Cookie:admin=true
// GET /api/track?tracking_id=123%0D%0AX-XSS-Protection:0%0D%0AContent-Type:text/html%0D%0A%0D%0A<script>alert('XSS')</script>
// POST /set-theme with theme="dark\r\nSet-Cookie: session_id=hijacked"
const express = require('express');
const { URL } = require('url');
const app = express();

// Utility functions for header security
function sanitizeHeaderValue(input) {
    if (typeof input !== 'string') {
        throw new Error('Header value must be a string');
    }
    
    // Remove CRLF and control characters
    const sanitized = input
        .replace(/[\r\n]/g, '')  // Remove CRLF
        .replace(/[\x00-\x1f\x7f-\x9f]/g, '')  // Remove control chars
        .trim();
    
    if (sanitized.length > 1000) {
        throw new Error('Header value too long');
    }
    
    return sanitized;
}

function validateUrl(urlString) {
    try {
        const url = new URL(urlString);
        
        // Only allow HTTP/HTTPS
        if (!['http:', 'https:'].includes(url.protocol)) {
            return false;
        }
        
        // Optional: Whitelist allowed domains
        const allowedDomains = ['example.com', 'trusted-site.com'];
        if (!allowedDomains.includes(url.hostname)) {
            return false;
        }
        
        return true;
    } catch (error) {
        return false;
    }
}

function validateTrackingId(trackingId) {
    // Only allow alphanumeric characters and hyphens
    return /^[a-zA-Z0-9-]+$/.test(trackingId) && trackingId.length <= 50;
}

// Security headers middleware
function addSecurityHeaders(req, res, next) {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('Content-Security-Policy', 
        "default-src 'self'; script-src 'self'");
    next();
}

app.use(addSecurityHeaders);
app.use(express.json({ limit: '1mb' }));

// SECURE: Validated redirect
app.get('/redirect', (req, res) => {
    try {
        const url = req.query.url;
        
        if (!url || typeof url !== 'string') {
            return res.status(400).send('URL parameter required');
        }
        
        const sanitizedUrl = sanitizeHeaderValue(url);
        
        if (!validateUrl(sanitizedUrl)) {
            return res.status(400).send('Invalid or untrusted URL');
        }
        
        res.redirect(sanitizedUrl);
    } catch (error) {
        console.error('Redirect error:', error.message);
        res.status(400).send('Invalid redirect request');
    }
});

// SECURE: Validated custom headers
app.get('/api/track', (req, res) => {
    try {
        const trackingId = req.query.tracking_id;
        const source = req.query.source;
        
        // Validate tracking ID
        if (!trackingId || !validateTrackingId(trackingId)) {
            return res.status(400).json({ error: 'Invalid tracking ID' });
        }
        
        // Validate and sanitize source
        if (!source || typeof source !== 'string') {
            return res.status(400).json({ error: 'Source parameter required' });
        }
        
        const sanitizedSource = sanitizeHeaderValue(source);
        
        // Whitelist allowed sources
        const allowedSources = ['web', 'mobile', 'api', 'email'];
        if (!allowedSources.includes(sanitizedSource)) {
            return res.status(400).json({ error: 'Invalid source' });
        }
        
        // Safe header setting
        res.setHeader('X-Tracking-ID', trackingId);
        res.setHeader('X-Source', sanitizedSource);
        
        res.json({ 
            message: 'Tracking recorded',
            tracking_id: trackingId,
            source: sanitizedSource
        });
    } catch (error) {
        console.error('Tracking error:', error.message);
        res.status(400).json({ error: 'Invalid tracking request' });
    }
});

// SECURE: Validated cookie setting
app.post('/set-theme', (req, res) => {
    try {
        const { theme, preferences } = req.body;
        
        // Validate theme
        const allowedThemes = ['light', 'dark', 'auto'];
        if (!theme || !allowedThemes.includes(theme)) {
            return res.status(400).send('Invalid theme');
        }
        
        // Validate preferences (JSON string)
        let validatedPreferences = '{}';
        if (preferences) {
            try {
                const parsed = JSON.parse(preferences);
                // Validate preference structure
                if (typeof parsed === 'object' && parsed !== null) {
                    validatedPreferences = JSON.stringify(parsed);
                }
            } catch (e) {
                return res.status(400).send('Invalid preferences format');
            }
        }
        
        // Safe cookie setting with security options
        res.cookie('theme', theme, {
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'strict',
            maxAge: 30 * 24 * 60 * 60 * 1000  // 30 days
        });
        
        res.cookie('preferences', validatedPreferences, {
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'strict',
            maxAge: 30 * 24 * 60 * 60 * 1000
        });
        
        res.json({ 
            message: 'Theme updated successfully',
            theme: theme
        });
    } catch (error) {
        console.error('Theme update error:', error.message);
        res.status(400).send('Invalid theme update request');
    }
});

// Error handling middleware
app.use((error, req, res, next) => {
    console.error('Application error:', error);
    res.status(500).json({ error: 'Internal server error' });
});

module.exports = app;

💡 Why This Fix Works

The vulnerable code directly uses user input in HTTP headers without validation. The secure version implements comprehensive input validation, header sanitization, and uses framework security features.

from django.http import HttpResponse, HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render
import json

# VULNERABLE: Direct user input in file download headers
def download_file(request):
    filename = request.GET.get('filename', 'default.txt')
    
    response = HttpResponse(content_type='application/octet-stream')
    # Dangerous: Direct filename inclusion
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    response.write(b'File content here')
    return response

# VULNERABLE: User input in custom response headers
def api_response(request):
    user_agent = request.META.get('HTTP_USER_AGENT', '')
    correlation_id = request.GET.get('correlation_id', '')
    
    response = HttpResponse('API Response')
    # No validation of user input
    response['X-User-Agent'] = user_agent
    response['X-Correlation-ID'] = correlation_id
    return response

# VULNERABLE: Redirect with user-controlled URL
def external_redirect(request):
    redirect_url = request.GET.get('url', '/')
    
    # No validation before redirect
    return HttpResponseRedirect(redirect_url)

@csrf_exempt
def set_user_data(request):
    if request.method == 'POST':
        user_data = json.loads(request.body)
        name = user_data.get('name', '')
        
        response = HttpResponse('Data saved')
        # Direct inclusion in cookie
        response.set_cookie('username', name)
        return response

# Malicious requests:
# GET /download?filename="test.txt\"\r\nContent-Type: text/html\r\n\r\n<script>alert('XSS')</script>"
# GET /api?correlation_id=123\r\nSet-Cookie: admin=true
# GET /redirect?url=javascript:alert('XSS')
# POST /set-user with name containing CRLF injection
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render
from urllib.parse import quote, urlparse
import json
import re
import logging

logger = logging.getLogger(__name__)

# Security utility functions
def sanitize_header_value(value):
    """Remove CRLF and control characters from header values"""
    if not isinstance(value, str):
        raise ValueError("Header value must be a string")
    
    # Remove CRLF and control characters
    sanitized = re.sub(r'[\r\n\x00-\x1f\x7f-\x9f]', '', value)
    
    # Length validation
    if len(sanitized) > 1000:
        raise ValueError("Header value too long")
    
    return sanitized.strip()

def validate_filename(filename):
    """Validate filename for safe download"""
    if not filename or len(filename) > 255:
        return False
    
    # Remove dangerous characters, keep only safe ones
    safe_filename = re.sub(r'[^\w\.-]', '_', filename)
    
    # Prevent directory traversal
    if '..' in safe_filename or safe_filename.startswith('.'):
        return False
    
    return safe_filename

def validate_url(url):
    """Validate URL for safe redirect"""
    try:
        parsed = urlparse(url)
        
        # Only allow HTTP/HTTPS
        if parsed.scheme not in ['http', 'https']:
            return False
        
        # Whitelist allowed domains (customize as needed)
        allowed_domains = ['example.com', 'trusted-site.com']
        if parsed.netloc and parsed.netloc not in allowed_domains:
            return False
        
        return True
    except Exception:
        return False

def validate_correlation_id(correlation_id):
    """Validate correlation ID format"""
    if not correlation_id:
        return False
    
    # Only allow alphanumeric and hyphens
    return re.match(r'^[a-zA-Z0-9-]+$', correlation_id) and len(correlation_id) <= 50

# SECURE: File download with proper header encoding
def download_file(request):
    try {
        filename = request.GET.get('filename', 'default.txt')
        
        # Validate and sanitize filename
        safe_filename = validate_filename(filename)
        if not safe_filename:
            return HttpResponse('Invalid filename', status=400)
        
        response = HttpResponse(content_type='application/octet-stream')
        
        # Proper Content-Disposition header encoding
        ascii_filename = safe_filename.encode('ascii', 'ignore').decode('ascii')
        utf8_filename = quote(safe_filename)
        
        response['Content-Disposition'] = (
            f'attachment; filename="{ascii_filename}"; '
            f'filename*=UTF-8\'\''{utf8_filename}'
        )
        
        # Additional security headers
        response['X-Content-Type-Options'] = 'nosniff'
        response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
        
        response.write(b'File content here')
        return response
        
    except Exception as e:
        logger.error(f"File download error: {e}")
        return HttpResponse('Download failed', status=500)

# SECURE: API response with validated headers
def api_response(request):
    try {
        # Get and validate correlation ID
        correlation_id = request.GET.get('correlation_id', '')
        
        if correlation_id and not validate_correlation_id(correlation_id):
            return JsonResponse({'error': 'Invalid correlation ID'}, status=400)
        
        # Get user agent (limit length and sanitize)
        user_agent = request.META.get('HTTP_USER_AGENT', '')[:200]
        if user_agent:
            user_agent = sanitize_header_value(user_agent)
        
        response = JsonResponse({'message': 'API Response', 'status': 'success'})
        
        # Safe header setting
        if user_agent:
            response['X-User-Agent-Hash'] = str(hash(user_agent))  # Don't reflect raw UA
        
        if correlation_id:
            response['X-Correlation-ID'] = correlation_id
        
        # Security headers
        response['X-Content-Type-Options'] = 'nosniff'
        response['X-XSS-Protection'] = '1; mode=block'
        
        return response
        
    except Exception as e:
        logger.error(f"API response error: {e}")
        return JsonResponse({'error': 'Request failed'}, status=500)

# SECURE: Validated redirect
def external_redirect(request):
    try {
        redirect_url = request.GET.get('url', '/')
        
        if not redirect_url:
            return HttpResponse('URL parameter required', status=400)
        
        # Sanitize the URL
        sanitized_url = sanitize_header_value(redirect_url)
        
        # Validate URL format and domain
        if not validate_url(sanitized_url):
            return HttpResponse('Invalid or untrusted URL', status=400)
        
        # Log redirect for monitoring
        logger.info(f"Redirect to {sanitized_url} from IP {request.META.get('REMOTE_ADDR')}")
        
        return HttpResponseRedirect(sanitized_url)
        
    except Exception as e:
        logger.error(f"Redirect error: {e}")
        return HttpResponse('Redirect failed', status=400)

@csrf_exempt
def set_user_data(request):
    if request.method == 'POST':
        try {
            user_data = json.loads(request.body)
            name = user_data.get('name', '')
            
            # Validate name
            if not name or len(name) > 100:
                return JsonResponse({'error': 'Invalid name'}, status=400)
            
            # Sanitize name for safe storage
            safe_name = re.sub(r'[^\w\s.-]', '', name).strip()
            
            if not safe_name:
                return JsonResponse({'error': 'Invalid name format'}, status=400)
            
            response = JsonResponse({'message': 'Data saved successfully'})
            
            # Safe cookie setting with security options
            response.set_cookie(
                'username', 
                safe_name,
                max_age=3600,  # 1 hour
                secure=request.is_secure(),  # HTTPS only in production
                httponly=True,  # Prevent XSS access
                samesite='Strict'  # CSRF protection
            )
            
            return response
            
        except json.JSONDecodeError:
            return JsonResponse({'error': 'Invalid JSON'}, status=400)
        except Exception as e:
            logger.error(f"User data error: {e}")
            return JsonResponse({'error': 'Request failed'}, status=500)
    
    return JsonResponse({'error': 'Method not allowed'}, status=405)

# Middleware for additional security headers
class SecurityHeadersMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        response = self.get_response(request)
        
        # Add security headers to all responses
        if not response.has_header('X-Content-Type-Options'):
            response['X-Content-Type-Options'] = 'nosniff'
        
        if not response.has_header('X-Frame-Options'):
            response['X-Frame-Options'] = 'DENY'
        
        if not response.has_header('X-XSS-Protection'):
            response['X-XSS-Protection'] = '1; mode=block'
        
        return response

💡 Why This Fix Works

The vulnerable Django code directly includes user input in HTTP headers without validation. The secure version implements proper input validation, header sanitization, URL validation, and security headers.

import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.core.io.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Cookie;

@RestController
public class VulnerableController {
    
    // VULNERABLE: File download with user-controlled filename
    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile(@RequestParam String filename) {
        // No validation of filename
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Disposition", 
                   "attachment; filename=\"" + filename + "\"");
        
        return ResponseEntity.ok()
                .headers(headers)
                .body(getFileResource(filename));
    }
    
    // VULNERABLE: Custom headers with user input
    @GetMapping("/api/data")
    public ResponseEntity<String> getData(
            @RequestParam String source,
            @RequestParam String trackingId,
            @RequestHeader("User-Agent") String userAgent) {
        
        HttpHeaders headers = new HttpHeaders();
        // Direct inclusion without validation
        headers.add("X-Data-Source", source);
        headers.add("X-Tracking-ID", trackingId);
        headers.add("X-Original-UA", userAgent);
        
        return new ResponseEntity<>("Data response", headers, HttpStatus.OK);
    }
    
    // VULNERABLE: Cookie setting with user data
    @PostMapping("/preferences")
    public ResponseEntity<String> setPreferences(
            @RequestBody Map<String, String> preferences,
            HttpServletResponse response) {
        
        String theme = preferences.get("theme");
        String language = preferences.get("language");
        
        // No validation before setting cookies
        Cookie themeCookie = new Cookie("theme", theme);
        Cookie langCookie = new Cookie("language", language);
        
        response.addCookie(themeCookie);
        response.addCookie(langCookie);
        
        return ResponseEntity.ok("Preferences saved");
    }
}

// Malicious inputs:
// filename: "file.txt\"\r\nContent-Type: text/html\r\n\r\n<script>alert('XSS')</script>"
// source: "web\r\nSet-Cookie: admin=true"
// theme: "dark\r\nSet-Cookie: session=hijacked"
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.core.io.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Cookie;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class SecureController {
    
    private static final Logger logger = LoggerFactory.getLogger(SecureController.class);
    
    // Validation patterns
    private static final Pattern CRLF_PATTERN = Pattern.compile("[\\r\\n]");
    private static final Pattern CONTROL_CHARS = Pattern.compile("[\\x00-\\x1f\\x7f-\\x9f]");
    private static final Pattern SAFE_FILENAME = Pattern.compile("^[a-zA-Z0-9._-]+$");
    private static final Pattern SAFE_TRACKING_ID = Pattern.compile("^[a-zA-Z0-9-]+$");
    
    // Whitelists
    private static final Set<String> ALLOWED_SOURCES = Set.of("web", "mobile", "api", "email");
    private static final Set<String> ALLOWED_THEMES = Set.of("light", "dark", "auto");
    private static final Set<String> ALLOWED_LANGUAGES = Set.of("en", "es", "fr", "de", "zh");
    
    // Utility methods
    private String sanitizeHeaderValue(String input) {
        if (input == null || input.isEmpty()) {
            return "";
        }
        
        // Remove CRLF and control characters
        String sanitized = CRLF_PATTERN.matcher(input).replaceAll("");
        sanitized = CONTROL_CHARS.matcher(sanitized).replaceAll("");
        
        // Length validation
        if (sanitized.length() > 1000) {
            throw new IllegalArgumentException("Header value too long");
        }
        
        return sanitized.trim();
    }
    
    private String validateFilename(String filename) {
        if (filename == null || filename.isEmpty() || filename.length() > 255) {
            throw new IllegalArgumentException("Invalid filename length");
        }
        
        // Remove dangerous characters
        String safe = filename.replaceAll("[^\\w.-]", "_");
        
        // Prevent directory traversal
        if (safe.contains("..") || safe.startsWith(".")) {
            throw new IllegalArgumentException("Invalid filename format");
        }
        
        return safe;
    }
    
    private String encodeFilenameForHeader(String filename) {
        try {
            String ascii = filename.replaceAll("[^\\x20-\\x7E]", "_");
            String utf8 = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString());
            
            return String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
                               ascii.replace("\"", "\\\\\""), utf8);
        } catch (Exception e) {
            throw new RuntimeException("Filename encoding failed", e);
        }
    }
    
    // SECURE: File download with proper validation
    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile(@RequestParam String filename) {
        try {
            // Validate and sanitize filename
            String safeFilename = validateFilename(filename);
            
            HttpHeaders headers = new HttpHeaders();
            
            // Proper Content-Disposition encoding
            headers.add("Content-Disposition", encodeFilenameForHeader(safeFilename));
            
            // Security headers
            headers.add("X-Content-Type-Options", "nosniff");
            headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
            
            Resource resource = getFileResource(safeFilename);
            if (resource == null || !resource.exists()) {
                return ResponseEntity.notFound().build();
            }
            
            return ResponseEntity.ok()
                    .headers(headers)
                    .body(resource);
                    
        } catch (IllegalArgumentException e) {
            logger.warn("Invalid download request: {}", e.getMessage());
            return ResponseEntity.badRequest().build();
        } catch (Exception e) {
            logger.error("Download error", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    
    // SECURE: API with validated headers
    @GetMapping("/api/data")
    public ResponseEntity<String> getData(
            @RequestParam String source,
            @RequestParam String trackingId,
            @RequestHeader(value = "User-Agent", required = false) String userAgent) {
        
        try {
            // Validate source against whitelist
            if (!ALLOWED_SOURCES.contains(source)) {
                return ResponseEntity.badRequest()
                    .body("Invalid source. Allowed: " + ALLOWED_SOURCES);
            }
            
            // Validate tracking ID format
            if (!SAFE_TRACKING_ID.matcher(trackingId).matches() || trackingId.length() > 50) {
                return ResponseEntity.badRequest().body("Invalid tracking ID format");
            }
            
            HttpHeaders headers = new HttpHeaders();
            
            // Safe header setting
            headers.add("X-Data-Source", source);  // Already validated
            headers.add("X-Tracking-ID", trackingId);  // Already validated
            
            // Don't reflect User-Agent directly, use hash instead
            if (userAgent != null && !userAgent.isEmpty()) {
                String safeUA = sanitizeHeaderValue(userAgent.substring(0, Math.min(200, userAgent.length())));
                headers.add("X-UA-Hash", String.valueOf(safeUA.hashCode()));
            }
            
            // Security headers
            headers.add("X-Content-Type-Options", "nosniff");
            headers.add("X-XSS-Protection", "1; mode=block");
            
            return new ResponseEntity<>("Data response", headers, HttpStatus.OK);
            
        } catch (Exception e) {
            logger.error("API data error", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Request failed");
        }
    }
    
    // SECURE: Cookie setting with validation
    @PostMapping("/preferences")
    public ResponseEntity<String> setPreferences(
            @RequestBody Map<String, String> preferences,
            HttpServletResponse response) {
        
        try {
            String theme = preferences.get("theme");
            String language = preferences.get("language");
            
            // Validate theme
            if (theme != null && !ALLOWED_THEMES.contains(theme)) {
                return ResponseEntity.badRequest()
                    .body("Invalid theme. Allowed: " + ALLOWED_THEMES);
            }
            
            // Validate language
            if (language != null && !ALLOWED_LANGUAGES.contains(language)) {
                return ResponseEntity.badRequest()
                    .body("Invalid language. Allowed: " + ALLOWED_LANGUAGES);
            }
            
            // Set secure cookies
            if (theme != null) {
                Cookie themeCookie = new Cookie("theme", theme);
                themeCookie.setHttpOnly(true);
                themeCookie.setSecure(true);  // HTTPS only
                themeCookie.setPath("/");
                themeCookie.setMaxAge(30 * 24 * 60 * 60);  // 30 days
                response.addCookie(themeCookie);
            }
            
            if (language != null) {
                Cookie langCookie = new Cookie("language", language);
                langCookie.setHttpOnly(true);
                langCookie.setSecure(true);
                langCookie.setPath("/");
                langCookie.setMaxAge(30 * 24 * 60 * 60);
                response.addCookie(langCookie);
            }
            
            // Security headers
            response.setHeader("X-Content-Type-Options", "nosniff");
            response.setHeader("X-Frame-Options", "DENY");
            
            return ResponseEntity.ok("Preferences saved successfully");
            
        } catch (Exception e) {
            logger.error("Preferences error", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Failed to save preferences");
        }
    }
    
    private Resource getFileResource(String filename) {
        // Implementation depends on your file storage
        return null;
    }
}

💡 Why This Fix Works

The vulnerable Spring Boot code directly includes user input in HTTP headers without validation. The secure version implements comprehensive input validation, whitelisting, proper encoding, and security headers.

Why it happens

The most common cause is directly including user input in HTTP response headers without validating or sanitizing for CRLF characters. This commonly occurs in redirect URLs, cookie values, custom headers, and content disposition headers.

Root causes

Unvalidated User Input in HTTP Headers

The most common cause is directly including user input in HTTP response headers without validating or sanitizing for CRLF characters. This commonly occurs in redirect URLs, cookie values, custom headers, and content disposition headers.

Preview example – JAVASCRIPT
// Vulnerable: Direct user input in redirect header
app.get('/redirect', (req, res) => {
    const redirectUrl = req.query.url;
    res.setHeader('Location', redirectUrl);  // Can inject CRLF
    res.status(302).send();
});
// Malicious input: url=evil.com%0D%0ASet-Cookie:+session=hijacked

Dynamic Cookie Generation with User Data

Applications that set cookie values based on user input without proper encoding are vulnerable to header injection. Attackers can inject additional cookies, session tokens, or other headers through cookie manipulation.

Preview example – PYTHON
# Vulnerable cookie setting
def set_user_preference(request, preference_value):
    response = HttpResponse("Preference saved")
    # Direct inclusion of user input in cookie
    response.set_cookie('user_pref', preference_value)
    return response
# Malicious input: preference_value="value\r\nSet-Cookie: admin=true"

Custom Header Injection in API Responses

REST APIs and web applications that add custom headers based on user input or request parameters without validation can be exploited to inject malicious headers or split HTTP responses.

Preview example – JAVA
// Vulnerable: Custom header with user input
@RequestMapping("/api/data")
public ResponseEntity<String> getData(@RequestParam String source) {
    HttpHeaders headers = new HttpHeaders();
    // Dangerous: User input directly in header
    headers.add("X-Data-Source", source);
    return new ResponseEntity<>("data", headers, HttpStatus.OK);
}

File Download Headers with User-Controlled Filenames

File download functionality that uses user-provided filenames in Content-Disposition headers without proper encoding can be exploited for header injection and response splitting attacks.

Preview example – PYTHON
# Vulnerable file download
def download_file(request, filename):
    response = HttpResponse(content_type='application/octet-stream')
    # Direct filename inclusion without encoding
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    return response
# Malicious input: filename="file.txt\"\r\nX-XSS-Protection: 0\r\nContent-Type: text/html\r\n\r\n<script>alert('XSS')</script>"

Fixes

1

Validate and Sanitize Header Values

Implement strict validation for all user input that will be included in HTTP headers. Remove or encode CRLF characters (\r\n), validate header value formats, and use whitelist validation for expected values where possible.

View implementation – JAVASCRIPT
function sanitizeHeaderValue(input) {
    if (typeof input !== 'string') {
        throw new Error('Header value must be a string');
    }
    
    // Remove CRLF characters and other dangerous chars
    const sanitized = input
        .replace(/[\r\n]/g, '')  // Remove CRLF
        .replace(/[\x00-\x1f\x7f-\x9f]/g, '')  // Remove control chars
        .trim();
    
    // Length validation
    if (sanitized.length > 1000) {
        throw new Error('Header value too long');
    }
    
    return sanitized;
}

// Usage
app.get('/redirect', (req, res) => {
    try {
        const redirectUrl = sanitizeHeaderValue(req.query.url);
        
        // Additional URL validation
        const url = new URL(redirectUrl);
        if (!['http:', 'https:'].includes(url.protocol)) {
            throw new Error('Invalid protocol');
        }
        
        res.redirect(redirectUrl);
    } catch (error) {
        res.status(400).send('Invalid redirect URL');
    }
});
2

Use Framework-Provided Header Setting Methods

Use web framework methods that automatically handle header encoding and validation rather than manually constructing headers. Most modern frameworks provide safe methods for setting headers that prevent injection attacks.

View implementation – PYTHON
from django.http import HttpResponse
from urllib.parse import quote
import re

def safe_file_download(request, filename):
    # Validate filename
    if not filename or len(filename) > 255:
        return HttpResponse('Invalid filename', status=400)
    
    # Remove dangerous characters
    safe_filename = re.sub(r'[^\w\.-]', '_', filename)
    
    # Use Django's safe header setting
    response = HttpResponse(content_type='application/octet-stream')
    
    # Proper encoding for Content-Disposition header
    response['Content-Disposition'] = (
        f'attachment; filename="{safe_filename}"; '
        f'filename*=UTF-8\'\''{quote(safe_filename)}'
    )
    
    return response
3

Implement Header Value Encoding

Properly encode header values according to HTTP specifications. Use percent-encoding for URLs, proper quote escaping for quoted strings, and UTF-8 encoding for international characters in headers.

View implementation – JAVA
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;

public class SafeHeaderUtils {
    private static final Pattern CRLF_PATTERN = Pattern.compile("[\r\n]");
    private static final Pattern CONTROL_CHARS = Pattern.compile("[\x00-\x1f\x7f-\x9f]");
    
    public static String sanitizeHeaderValue(String input) {
        if (input == null || input.isEmpty()) {
            return "";
        }
        
        // Remove CRLF and control characters
        String sanitized = CRLF_PATTERN.matcher(input).replaceAll("");
        sanitized = CONTROL_CHARS.matcher(sanitized).replaceAll("");
        
        // Length validation
        if (sanitized.length() > 1000) {
            throw new IllegalArgumentException("Header value too long");
        }
        
        return sanitized.trim();
    }
    
    public static String encodeFilename(String filename) {
        // Sanitize first
        String safe = sanitizeHeaderValue(filename);
        
        // Encode for Content-Disposition header
        try {
            String encoded = URLEncoder.encode(safe, StandardCharsets.UTF_8.toString());
            return String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", 
                               safe.replaceAll("\"", "\\\\\""), encoded);
        } catch (Exception e) {
            throw new RuntimeException("Encoding failed", e);
        }
    }
}

@RestController
public class SecureApiController {
    
    @GetMapping("/api/data")
    public ResponseEntity<String> getData(@RequestParam String source) {
        try {
            String sanitizedSource = SafeHeaderUtils.sanitizeHeaderValue(source);
            
            HttpHeaders headers = new HttpHeaders();
            headers.add("X-Data-Source", sanitizedSource);
            
            return new ResponseEntity<>("data", headers, HttpStatus.OK);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("Invalid source parameter");
        }
    }
}
4

Use Content Security Policy and Security Headers

Implement Content Security Policy (CSP) and other security headers to mitigate the impact of successful header injection attacks. These headers can prevent XSS execution and other malicious activities even if header injection occurs.

View implementation – JAVASCRIPT
// Security headers middleware
function addSecurityHeaders(req, res, next) {
    // Prevent MIME type sniffing
    res.setHeader('X-Content-Type-Options', 'nosniff');
    
    // Enable XSS protection
    res.setHeader('X-XSS-Protection', '1; mode=block');
    
    // Prevent clickjacking
    res.setHeader('X-Frame-Options', 'DENY');
    
    // Content Security Policy
    res.setHeader('Content-Security-Policy', 
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
    
    // Prevent caching of sensitive responses
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
    res.setHeader('Pragma', 'no-cache');
    
    next();
}

app.use(addSecurityHeaders);
5

Implement Response Validation and Monitoring

Monitor HTTP responses for suspicious patterns, validate response headers before sending, and implement logging for potential header injection attempts. Use automated tools to detect response splitting patterns.

View implementation – PYTHON
import logging
import re
from typing import Dict, Any

class ResponseSecurityMonitor:
    def __init__(self):
        self.suspicious_patterns = [
            r'\r\n',  # CRLF injection
            r'\n\r',  # Reverse CRLF
            r'Set-Cookie:',  # Cookie injection
            r'Location:',  # Redirect injection
            r'Content-Type:',  # Content type manipulation
            r'<script',  # XSS attempts
        ]
        
    def validate_response_headers(self, headers: Dict[str, str]) -> bool:
        """Validate response headers for injection attempts"""
        for header_name, header_value in headers.items():
            # Check header name
            if not self._is_safe_header_name(header_name):
                logging.warning(f"Suspicious header name: {header_name}")
                return False
            
            # Check header value
            if not self._is_safe_header_value(header_value):
                logging.warning(f"Suspicious header value in {header_name}: {header_value}")
                return False
        
        return True
    
    def _is_safe_header_name(self, name: str) -> bool:
        # HTTP header name validation
        if not re.match(r'^[a-zA-Z0-9-]+$', name):
            return False
        return len(name) <= 100
    
    def _is_safe_header_value(self, value: str) -> bool:
        # Check for injection patterns
        for pattern in self.suspicious_patterns:
            if re.search(pattern, value, re.IGNORECASE):
                return False
        
        # Check for control characters
        if re.search(r'[\x00-\x1f\x7f-\x9f]', value):
            return False
        
        return len(value) <= 2048
    
    def log_response_attempt(self, headers: Dict[str, str], user_ip: str):
        """Log response for security monitoring"""
        header_summary = ', '.join([f"{k}: {v[:50]}..." if len(v) > 50 else f"{k}: {v}" 
                                   for k, v in headers.items()])
        
        logging.info(f"Response headers from {user_ip}: {header_summary}")

# Usage in Flask application
from flask import Flask, request, make_response

app = Flask(__name__)
monitor = ResponseSecurityMonitor()

@app.after_request
def validate_response(response):
    headers_dict = dict(response.headers)
    
    if not monitor.validate_response_headers(headers_dict):
        logging.error("Suspicious response headers detected")
        # Return safe error response
        return make_response("Invalid response", 400)
    
    monitor.log_response_attempt(headers_dict, request.remote_addr)
    return response

Detect This Vulnerability in Your Code

Sourcery automatically identifies http response splitting and header injection and many other security issues in your codebase.