HTTP response splitting

HTTP response splitting

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.

References