Attaques par en-tête d'hôte HTTP

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Comment fait le code d'une application web pour savoir sur quel domaine il opère ? Dans bien des cas, il se base sur la valeur de l'en-tête HTTP Host.

Or cette valeur n'est pas du tout fiable et peut être surchargée par le premier pimpin venu :

curl --header "Host: attacker.com" http://127.0.0.1:8000

All the headers in HTTP requests are belong to us.

Une fausse valeur d'hôte a la capacité d'être exploitée par un attaquant via Cross-Site Request Forgery, empoisonnement de cache, empoisonnement des liens des e-mails etc.

On parle alors d'attaques par en-tête HTTP Host. En anglais : Host Header Injection ou Host Header Attack.

Django permet de mitiger cette attaque avec une liste blanche via le setting ALLOWED_HOSTS inspecté dans get_host à chaque requête si CommonMiddleware est installé.

Si vous êtes derrière un proxy qui vous force à utiliser l'en-tête X-Forwarded-Host, alors il vous faudra le déclarer explicitement dans les settings de Django. Cette syntaxe d'en-têtes non standards avec des préfixes en X- est désormais dépréciée (RFC 6648) et remplacée (en théorie) par une alternative détaillée dans le RFC 7239, mais rien n'est plus permanent qu'une solution temporaire :

[…] when non-standard headers prefixed with X- become standard, removing the X- prefix breaks backwards compatibility, forcing application protocols to support both names […]

Les attaques par en-tête HTTP Host sont exploitées dans le wild en masse par tous les outils de hacking automatisés.

Avec Django ça se voit au nombre d'erreurs Invalid HTTP_HOST header qui remontent.

Une fois que votre application Django est bien configurée, il est possible de refuser les requêtes HTTP avec des en-têtes Host illégitimes en amont au niveau d'Nginx en renvoyant un code de statut 444 :

if ($http_host != my.domain.com) {
    return 444;
}

Sinon, si vous n'avez pas la main sur le serveur web, vous pouvez aussi taire ces erreurs en modifiant la configuration du logging :

'handlers': {
    'null': {
        'class': 'logging.NullHandler',
    },
},
'loggers': {
    'django.security.DisallowedHost': {
        'handlers': ['null'],
        'propagate': False,
    },
},

Enfin, pour faire taire Sentry, vous pouvez utiliser ignore_logger :

from sentry_sdk.integrations.logging import ignore_logger

ignore_logger("django.security.DisallowedHost")

Avant Recherche plein texte PostgreSQL Après OAuth et OpenID Connect

Tag Kemar Joint