Protect HTTP response headers from response splitting
Description
Response splitting injects newline characters into an HTTP response header to separate the response and add attacker-controlled output. If the framework or server permits those characters, this can enable cross-site scripting (XSS) or cache poisoning. Django and Werkzeug reject newlines in headers, so the unchecked examples below do not automatically permit response splitting; invalid input may instead raise an exception.
Potential impact
- XSS: Injected scripts may run in the client.
- Cache poisoning: Malicious content may be cached and served to other users.
- Information disclosure: Manipulated responses may expose sensitive data.
Remediation
- Validate header input and filter newline characters (
\r,\n). - Set headers through framework APIs and ensure newline characters are rejected.
Examples
Django
Before
python
# Django without application input validation
from django.http import HttpResponse
def unsafe_view(request):
user_input = request.GET.get('input', '')
response = HttpResponse()
response['X-My-Header'] = user_input
return response
After
python
# Django with newline removal
from django.http import HttpResponse
import re
def safe_view(request):
user_input = request.GET.get('input', '')
sanitized_input = re.sub(r'[\r\n]', '', user_input)
response = HttpResponse()
response['X-My-Header'] = sanitized_input
return response
Explanation:
- Before: Input is passed directly to a header. Whether newlines are accepted determines the risk of an error or header injection.
- After: Newlines are removed before setting the header. Also validate values for the header's intended purpose.
Flask
Before
python
# Flask without application input validation
from flask import Flask, request, Response
app = Flask(__name__)
@app.route('/unsafe')
def unsafe():
user_input = request.args.get('input', '')
response = Response()
response.headers['X-My-Header'] = user_input
return response
After
python
# Flask with newline removal
from flask import Flask, request, Response
import re
app = Flask(__name__)
@app.route('/safe')
def safe():
user_input = request.args.get('input', '')
sanitized_input = re.sub(r'[\r\n]', '', user_input)
response = Response()
response.headers['X-My-Header'] = sanitized_input
return response
Explanation:
- Before: Input is passed directly to a header. Whether newlines are accepted determines the risk of an error or header injection.
- After: Newlines are removed before setting the header. Also validate values for the header's intended purpose.
FastAPI
Before
python
# FastAPI without application input validation
from fastapi import FastAPI, Request, Response
app = FastAPI()
@app.get("/unsafe")
async def unsafe(request: Request):
user_input = request.query_params.get('input', '')
response = Response()
response.headers['X-My-Header'] = user_input
return response
After
python
# FastAPI with newline removal
from fastapi import FastAPI, Request, Response
import re
app = FastAPI()
@app.get("/safe")
async def safe(request: Request):
user_input = request.query_params.get('input', '')
sanitized_input = re.sub(r'[\r\n]', '', user_input)
response = Response()
response.headers['X-My-Header'] = sanitized_input
return response
Explanation:
- Before: Input is passed directly to a header. Whether newlines are accepted determines the risk of an error or header injection.
- After: Newlines are removed before setting the header. Also validate values for the header's intended purpose.