Ce que vous préféreriez ne pas avoir à savoir sur S3
(blog.plerion.com)- Dans les risques de fuite de données AWS, les accès non autorisés aux buckets S3 reviennent régulièrement, et l’ancien design de l’API ainsi que certains comportements exceptionnels rendent difficile un simple jugement « public/privé »
- De nombreuses opérations S3 ne sont pas appelées via un endpoint AWS générique, mais via l’URL du bucket elle-même ; avec une politique de bucket mal configurée, même une requête
curlnon authentifiée peut permettre des opérations dangereuses - Bloquer seulement
s3:ListBucketne suffit pas forcément à être en sécurité : des chemins commeListBucketVersions,ListMultipartUploadsoufetch-ownerpeuvent révéler des clés d’objets et des identifiants de compte - L’uploader peut influencer des propriétés d’objet comme la classe de stockage, les tags, Object Lock et certains en-têtes liés aux redirections ; des contrôles supplémentaires tels que des conditions IAM et des règles de cycle de vie sont donc nécessaires
- Se fier uniquement aux ACL et aux paramètres de blocage de l’accès public pour conclure qu’un bucket est privé peut faire manquer certains chemins d’accès ; des internautes peuvent accéder à des objets S3 via une distribution CloudFront ou un identity pool Cognito
Ancien design de l’API S3 et appels anonymes
- S3 étant l’un des premiers services d’AWS, il est robuste et bien testé, mais les traces d’une époque antérieure aux modèles de conception standardisés lui donnent une forme d’API différente de celle d’autres services AWS
- Certaines API S3 utilisent des endpoints génériques comme
s3.us-east-2.amazonaws.com, mais de nombreuses opérations doivent être envoyées directement à l’URL du bucket cible- Un exemple de listing de bucket est un
GET /avecHost: [bucketname].s3.amazonaws.com - La récupération des tags d’un bucket envoie aussi
GET /?taggingà l’hôte du bucket concerné
- Un exemple de listing de bucket est un
- Beaucoup de services AWS, comme EC2 ou DynamoDB, utilisent généralement un endpoint générique et transmettent la ressource cible via des en-têtes HTTP ou des paramètres
- Les buckets S3 prenant en charge à la fois l’accès public et l’accès authentifié, il n’est pas toujours clair quelles opérations d’API sont possibles sans authentification
- Si un exemple de politique de bucket autorise
Principal: "*"etAction: "s3:*"sur la ressource du bucket, une requête non authentifiée peut même supprimer le bucket- Exemple de requête :
curl -X DELETE https://[bucketname].s3-ap-southeast-2.amazonaws.com
- Exemple de requête :
- Certaines opérations ne prennent pas en charge les requêtes anonymes et renvoient une erreur comme
s3:GetBucketOwnershipControls does not support Anonymous requests! - Les requêtes API anonymes sont enregistrées dans CloudTrail sous le compte anonymous
- Si la requête n’est pas authentifiée, il est impossible d’identifier qui a supprimé le bucket, ou consulté la configuration de chiffrement ou l’état de journalisation
- Des chemins comme
/?logging,/?tagginget/?encryptionpeuvent aussi être testés dans un navigateur - Certaines opérations, comme
GetObjectTorrent, restent documentées mais ne peuvent plus être exécutées
Bloquer ListBucket ne suffit pas à empêcher l’exposition des clés d’objets
- Pour télécharger un objet S3, il faut connaître sa clé, qui s’utilise comme un chemin de fichier
- Une requête
GETà la racine du bucket peut, selon les conditions, renvoyer son contenu ; refusers3:ListBucketsemble donc être une défense courante - Même en combinant une ACL
public-readet une politique refusants3:ListBucket, il reste des chemins pour obtenir des clés d’objetsGET /?versions, c’est-à-dires3:ListBucketVersions, renvoie les métadonnées des versions d’objets dans le bucketGET /?uploads, c’est-à-dires3:ListMultipartUploads, renvoie la liste des multipart uploads en cours
- La documentation de
HeadBucketévoque la vérification de l’existence du bucket et des droits d’accès, mais en pratique elle vérifie si l’appelant a le droit d’effectuer l’actionListBucket - Se limiter à vérifier que
ListBucketest refusé peut faire manquer un risque d’exposition des clés d’objets S3
Coûts et exposition des multipart uploads non terminés
- Un multipart upload commence avec
create-multipart-upload, puis les parties sont envoyées avecupload-part - Les multipart uploads non terminés ne sont pas faciles à repérer dans la console web ; on peut les vérifier avec
/?uploadsouaws s3api list-multipart-uploads --bucket [bucket-name] - Si la requête de finalisation n’est pas envoyée avec succès, Amazon S3 n’assemble pas les parties et ne crée pas l’objet
- Les parties uploadées restent dans le compte jusqu’à ce que le multipart upload soit terminé ou interrompu
- Les parties stockées génèrent des frais de stockage S3
- Aucune méthode n’a été trouvée pour télécharger les parties d’un objet avant finalisation, mais leur suppression est possible
- AWS recommande d’appliquer une règle de cycle de vie qui supprime les uploads non terminés après un certain nombre de jours
- Lister les multipart uploads non terminés avec
/?uploadsrenvoie l’ARN du principal qui a lancé l’upload- Si l’on ne considère pas les identifiants comme les ID de compte ou les ARN comme sensibles, ce n’est pas forcément un problème
- Si l’on refuse d’exposer des identifiants utiles à un attaquant, cela peut être considéré comme une exposition
ACL et vérification de compte basée sur l’e-mail
- La documentation des ACL S3 conserve des traces de l’époque où les comptes AWS étaient identifiés par l’e-mail de l’utilisateur racine
- L’opération
PutBucketACLpeut désigner le grantee par adresse e-mail- Elle utilise
Type: AmazonCustomerByEmailetEmailAddress
- Elle utilise
- S’il n’existe pas de compte AWS associé à l’e-mail indiqué, une erreur
UnresolvableGrantByEmailAddressse produit- Le message d’erreur indique que « l’adresse e-mail fournie ne correspond à aucun compte enregistré »
- Ce comportement permet de vérifier si une adresse e-mail donnée possède un compte AWS enregistré
Classes de stockage et métadonnées d’objet que l’uploader peut choisir
- Les classes de stockage S3 s’appliquent aux objets, pas aux buckets
- Il n’existe pas de paramètre permettant de fixer la classe de stockage souhaitée au niveau du bucket ; l’entité qui effectue l’upload peut définir la classe de stockage de l’objet
- Exemple :
aws s3 cp "my.txt" "s3://mybucket/myobject.txt" --storage-class [CLASS]
- Exemple :
- L’uploader peut, parmi une liste prédéfinie, influencer les coûts de stockage et d’accès par Go supportés par le propriétaire du bucket
- Dans une politique IAM, la clé de condition
s3:x-amz-storage-classpermet de limiter les classes de stockage autorisées- L’exemple de politique n’autorise que
STANDARDpours3:PutObject
- L’exemple de politique n’autorise que
- En configurant une politique de cycle de vie, on peut transférer tous les objets vers une classe de stockage donnée après un certain délai
- Pour les uploads utilisant une URL pré-signée, AWS Signature Version 4 exige la signature de tous les en-têtes commençant par
X-Amz-- La classe de stockage est indiquée avec l’en-tête
x-amz-storage-class - Sauf implémentation très défaillante de l’application, il n’existe pas de moyen évident de la manipuler immédiatement
- La classe de stockage est indiquée avec l’en-tête
Tags, Object Lock et redirections sont aussi sous l’influence de l’uploader
- De nombreuses propriétés liées aux objets S3 sont contrôlées par l’uploader
- Les tags d’objet peuvent être définis au moment de l’upload
- Exemple :
--tagging "AllYourTags=AreBelong&To=Us"
- Exemple :
- Les systèmes qui automatisent des traitements selon les valeurs de tags peuvent être influencés par les tags créés par l’uploader
- Object Lock permet de définir la conservation d’un objet et un legal hold lorsque l’object locking est activé sur le bucket
- La commande d’exemple utilise
--object-lock-retain-until-date "2099-01-01T00:00:00+0000",--object-lock-legal-hold-status "ON"et--object-lock-mode "COMPLIANCE"
- La commande d’exemple utilise
- Dans les buckets où l’hébergement de site web statique est activé, une redirection ouverte est possible via la configuration du fichier uploadé
- Il faut aussi prêter attention à la liste complète des en-têtes pris en charge par
PutObject- Les URL pré-signées ont des limitations
- Dans les configurations reposant sur une identité Cognito et des politiques IAM, les requêtes peuvent être signées avec le contexte Cognito authentifié
Exposition du propriétaire du bucket et des identifiants de compte
- Pour vérifier si un ID de compte donné est propriétaire d’un bucket accessible, on peut ajouter l’en-tête
x-amz-expected-bucket-ownerà une requêteListBucket- Si l’ID de compte est incorrect,
AccessDeniedest renvoyé - Si l’ID de compte est correct et que l’appelant dispose du droit
ListBucket, la réponse est normale
- Si l’ID de compte est incorrect,
- Le paramètre
fetch-owner=truede l’APIListBucketajoute un élément Owner à chaque clé dans la réponse - L’
IDdansOwnerest une chaîne hexadécimale de 64 caractères que la documentation AWS appelle canonical user ID- Il s’agit d’une forme obfusquée de l’ID de compte AWS
- Si l’on met ce canonical user ID dans le champ
Principald’une politique IAM en tant queCanonicalUser, puis que l’on enregistre et actualise, il est interprété comme un ID de compte AWS ListBucketVersionsetListMultipartUploadsse comportent de manière similaire, même sansfetch-owner
Les clés d’objets S3 ressemblent à des noms de fichiers, mais fonctionnent différemment
- Les clés d’objets S3 sont sensibles à la casse
- Même si les noms semblent identiques, il est possible d’uploader plusieurs objets lorsque la casse diffère
- Si une application traite les clés d’objets S3 comme des noms de fichiers insensibles à la casse, des problèmes peuvent survenir
- L’application d’exemple stocke les mots de passe des utilisateurs dans des fichiers S3 et utilise le nom d’utilisateur comme nom de fichier
- À l’inscription, elle vérifie seulement l’existence du fichier ; lors du changement de mot de passe, elle convertit le nom d’utilisateur en minuscules avant d’écrire dans le fichier
- Même si
jeffexiste déjà, l’inscription deJEFFest possible, et l’utilisateurJEFFpeut écraser le fichier dejeffen changeant son mot de passe
- N’importe quel caractère UTF-8 peut être utilisé dans une clé d’objet S3
- Certains caractères peuvent poser problème dans certaines applications et certains protocoles
- Les espaces, les barres obliques et les signes pourcentage sont aussi valides dans les clés d’objets
Même un « bucket privé » apparent peut garder des chemins d’accès
- Même si les ACL sont désactivées, que la politique de ressource est strictement limitée et que le blocage de l’accès public est activé, un bucket peut rester accessible publiquement
- Le chemin le plus courant est une distribution Amazon CloudFront
- Lorsqu’un CDN est placé devant un bucket S3, l’intention est généralement de diffuser du contenu sur Internet
- Les outils de sécurité peuvent considérer qu’un bucket n’est pas public si sa politique de ressource est limitée à CloudFront
- Dans l’exemple,
get-bucket-policy-statusrenvoieIsPublic: false- Une requête envoyée directement au bucket renvoie
AccessDenied - La même requête envoyée au domaine de distribution CloudFront renvoie le contenu de l’objet
- Une requête envoyée directement au bucket renvoie
- Un identity pool Cognito peut aussi exposer un bucket dont la politique de ressource est limitée
- Après une connexion réussie, Cognito fournit des identifiants AWS temporaires pour un rôle préconfiguré
- Si ce rôle dispose des permissions
s3:ListBucketets3:GetObject, l’utilisateur peut appeler l’API S3
- Deux configurations Cognito peuvent relever de l’accès public
- Self-registration : si les internautes peuvent s’inscrire à l’application et se connecter, cela devient en pratique un accès public
- Guest access : fournit un identifiant unique et des identifiants AWS à des utilisateurs non authentifiés
- Un exemple d’accès invité suit le flux suivant : obtenir un
IdentityIdavecget-id, récupérer des identifiants temporaires avecget-credentials-for-identity, puis exécuteraws s3 lsavec ce profil - CloudFront et les identity pools Cognito sont effectivement souvent utilisés sur Internet, mais ce sont des chemins d’accès public rarement signalés par les outils de sécurité
1 commentaires
Avis de Hacker News
Il y a beaucoup de points intéressants, mais j’ai du mal à être d’accord avec l’idée de voir comme un défaut le fait qu’un système de fichiers soit sensible à la casse
À mes yeux, c’est comme ça que ça devrait être, et c’est plutôt le fait que macOS ne le soit pas qui m’agace
La sensibilité à la casse dans les noms de fichiers peut surprendre même les utilisateurs non techniques. Si quelqu’un dit avoir envoyé « Book Draft 1.docx » et que, dans la boîte mail, on trouve « Book draft 1.docx », on ne répond généralement pas « il me semble que vous avez envoyé un autre fichier »
Dans l’écriture aussi, la casse ne change généralement pas le sens. « Hi, how are you? » et « hi, how are you? » veulent dire la même chose, et les cas où une majuscule change le sens relèvent surtout de la distinction entre noms propres et noms communs, ce qui est rarement une préoccupation pour les noms de fichiers
Indépendamment des préférences personnelles, il est difficile de comprendre qu’un développeur ou un administrateur système soit surpris ou agacé par la sensibilité à la casse d’un système de fichiers. Si nécessaire, le développeur peut l’abstraire pour l’utilisateur final, comme dans des résultats de recherche
Ce n’est pas comme en programmation, où imposer un style de code peut aider la lisibilité. Même en programmation, avant que les IDE ne deviennent assez intelligents pour détecter les fautes de frappe dans les noms de variables, c’était souvent une source de bugs. L’un des points que j’aimais dans Pascal, contrairement à C, était de ne pas avoir à se soucier de la casse
On peut écrire les noms de fichiers dans le style qu’on veut et cette graphie est conservée, mais lors d’une recherche ou d’un traitement, on n’a pas besoin de se souvenir exactement de ce style. La recherche n’est en effet pas sensible à la casse
La sensibilité à la casse est plutôt simple ; ce qui est moins intuitif, c’est que les chemins S3 sont factices
S3 accepte l’envoi de « /builds/1/installer.exe » et affiche aussi une liste dans /builds, mais en réalité vous avez importé une seule clé appelée « /builds/1/installer.exe », dont le nom contient des « / »
Du coup, « /builds/1//installer.exe » et « /builds//1/installer.exe » peuvent aussi être envoyés et sont des fichiers complètement différents. Ce ne sont que des noms de clés ; il n’y a pas de vrais répertoires
[1] https://docs.aws.amazon.com/AmazonS3/latest/userguide/direct...
Cette année, notre système de production a aussi rencontré un bug étrange, et il a fallu cinq personnes pour en trouver la cause. C’était un objet dont le nom était littéralement « / », et le logiciel essayait de le traiter comme un chemin plutôt que comme un fichier
Il est difficile de faire confiance à S3 ou à d’autres services AWS. Rien n’est intuitif, il y a trop d’éléments en mouvement et trop de documentation à lire
Même comme ça, on peut finir, comme dans l’article, par tout rendre public par erreur. Je préférerais utiliser un service réellement simple comme Hetzner Storage Boxes ou DigitalOcean Spaces
J’ai récemment découvert que lorsqu’on envoie par pipe un fichier vidéo de plus de quelques Mo, le Location renvoyé omet https://. Il faut donc vérifier à chaque upload que Location commence par https, et l’ajouter si ce n’est pas le cas
Évidemment, dans les issues GitHub du client S3 Node, on répond « ça ressemble à un bug DigitalOcean », et sur les forums DigitalOcean, « ça ressemble à un bug du client S3 Node »
Une foule de fonctionnalités et de particularités, conçues à l’origine pour aider dans certains cas spécifiques, font désormais partie du protocole courant. J’ai l’impression que c’est le résultat d’une logique commerciale visant à ne faire fuir personne
Il faut aussi faire attention lorsqu’on supprime des dizaines de milliards d’objets. Appeler directement l’API de suppression peut coûter cher
À la place, on peut configurer gratuitement une règle de cycle de vie qui fixe l’expiration à maintenant, pour un wildcard ou pour tout le bucket. La facturation du stockage s’arrête alors immédiatement, et AWS se charge de la suppression
Cela évite aussi que les serveurs de l’API S3 se fassent marteler par le nombre de requêtes par seconde
Le fait que des uploads multipart échoués restent invisibles et génèrent quand même des frais de stockage si l’on ne configure pas explicitement une règle de cycle de vie est vraiment pénible
Je croyais que le S de “Simple” voulait dire simplicité
Ma proposition était que les parties des uploads incomplets ne restent que 24 heures après la dernière activité, sans facturation du stockage pendant ce délai. ahenry@ a refusé
Sur un très vieux serveur, un script cron lançait chaque nuit, depuis presque dix ans, un upload multipart. Il servait à pousser des sauvegardes dans un bucket qui stockait aussi du contenu uploadé par les utilisateurs, donc le fait qu’il grossisse un peu chaque jour semblait normal
Le script était « non fonctionnel », donc nous ne dépendions pas des données de sauvegarde ; les fichiers n’apparaissaient pas non plus dans S3, et la taille du bucket augmentait régulièrement sans excès. Puis, ce printemps, nous avons découvert qu’il stockait près de 3 To d’uploads multipart incomplets
Bien sûr, je sais que cette anecdote est remplie de mauvaises pratiques
Les débats sur la sensibilité ou non à la casse me semblent en général beaucoup trop anglocentrés
Autrement dit, surtout dans l’IT, les discussions liées aux langues sont trop souvent excessivement centrées sur l’anglais
En tant que personne dont l’anglais n’est pas la langue maternelle, je dirais que la programmation comporte déjà beaucoup trop de concepts et d’éléments. Mieux vaut ne pas ajouter de complexité en prenant aussi en compte 101 autres langues
Unicode et les fuseaux horaires sont des exemples typiques d’éléments visant à prendre en compte davantage de langues et de cultures en programmation, et au final ils créent les plus grosses douleurs pour tout le monde, y compris les programmeurs non anglophones
Je ne veux pas écrire des programmes dans ma langue maternelle. Encore moins si, en contrepartie, il faut tenir compte de toutes les grandes langues lorsqu’on programme. Que les discussions IT soient centrées sur l’anglais me va. La diversité, c’est de la complexité, et l’anglais n’est pas une langue que quelqu’un possède : c’est simplement un outil que les gens utilisent pour communiquer
Grâce à cette langue commune, je peux exprimer mes idées à beaucoup de personnes en Inde, en Chine, au Japon, en Amérique du Sud, etc. Dès lors qu’elles choisissent de parler anglais, elles possèdent elles aussi l’anglais. Inutile d’importer la politique de la diversité dans l’IT ; mieux vaut rester sur un plan technique
D’une certaine manière, ces deux syllabaires donnent l’impression d’être comme les alphabets majuscule/minuscule
Il y en a encore quelques-uns
Les uploads multipart ne peuvent pas être effectués depuis plusieurs machines disposant d’identifiants d’instance. Comme les principals diffèrent, ils ne peuvent pas accéder aux uploads multipart les uns des autres. Pour assembler un même upload multipart depuis plusieurs machines, il faut un véritable utilisateur IAM
Les requêtes LIST ne sont pas seulement lentes : en volume, elles coûtent très cher. Il existe des contournements comme « bucket inventory », mais ce n’est ni pratique ni bon marché
La création de buckets utilise DNS en interne, elle n’offre donc pas de cohérence lecture-après-écriture. Il arrive ainsi qu’un bucket ne soit pas accessible juste après sa création, ou qu’on ne puisse pas supprimer un bucket fraîchement créé avant d’avoir attendu suffisamment longtemps la propagation des changements. Voir https://github.com/julik/talks/blob/master/euruko-2019-no-su...
On peut créer simultanément un objet nommé « foo » et un objet nommé « foo/bar ». On se retrouve alors avec une structure où un fichier écrase un répertoire, ce qui rend impossible la portabilité des données du bucket vers une structure de système de fichiers
S3 est sensible à la casse, ce qui permet de créer des objets impossibles à transposer dans une structure de système de fichiers. Le stockage de fichiers de Rails supposait un stockage sensible à la casse, ce qui a provoqué de gros problèmes sur macOS, et a été corrigé en utilisant toujours des identifiants en minuscules
La plupart des configurations S3 autorisent GET mais pas HEAD. C’est probablement une façon d’empêcher la détection de l’existence d’un objet, sans certitude. Quoi qu’il en soit, le flux compatible avec les caches consistant à vérifier la taille d’un objet via une requête HEAD ne fonctionne pas, surtout avec des URL pré-signées. Il faut à la place contourner le problème avec un GET sur une plage très petite, par exemple en ne récupérant que le premier octet
Si vous générez beaucoup d’URL pré-signées, il est possible d’accélérer leur génération d’un facteur 10 à 40 : https://github.com/WeTransfer/wt_s3_signer
Vous payez tout de même les frais de stockage des uploads multipart inachevés. Il faut être particulièrement prudent si votre architecture permet aux utilisateurs de lancer ce type d’upload. Il existe un réglage pour supprimer automatiquement les uploads multipart inachevés après un certain délai ; activez-le si vous voulez vous éviter des ennuis
Paradoxalement, S3 a été révolutionnaire et reste un excellent produit à bien des niveaux. Mais comme il a beaucoup de fonctionnalités, il a aussi beaucoup de pièges
J’avais construit en Elixir un pipeline de post-traitement CSV en streaming qui modifie et injecte des colonnes avec Stream.transform(https://hexdocs.pm/elixir/Stream.html#transform/3). Les modules AWS et CSV d’Elixir traitent bien les données entrantes en streaming, mais si le volume total du flux sortant est inférieur à 5 Mio, le module AWS utilise un upload multipart, ce qui provoquait une erreur côté S3, à ma grande tristesse
Il y a un autre problème amusant qu’un collègue et moi avons analysé et diagnostiqué pendant plusieurs jours. S3 abandonne silencieusement les requêtes suivantes lorsqu’une même connexion TCP a envoyé 100 requêtes HTTP
https://github.com/aws/aws-sdk-go/issues/2825
C’est un schéma courant quand on veut du keep-alive pour les performances, tout en évitant que des clients restent connectés trop longtemps et créent des points chauds sur le répartiteur de charge
Il y a aussi le fait que S3, avec la classe de stockage standard, présente une latence élevée et n’est pas adapté au service web direct
Beaucoup de gens pensent qu’il suffit d’héberger directement depuis S3 des ressources de site web comme des images ou des polices, mais cela peut dégrader l’expérience utilisateur
« applications can achieve consistent small object latencies (and first-byte-out latencies for larger objects) of roughly 100–200 milliseconds. »
Source : https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimi...
Avec les CloudFront signed cookies, on peut aussi donner à un utilisateur donné un accès CDN uniquement au contenu qui lui appartient dans S3. C’est plutôt élégant
Cela permet de mettre en cache les assets fréquemment consultés afin de réduire la latence, et aussi de diminuer sensiblement les coûts
Le fait que l’uploader décide des règles est assez brutal. Sur un site web mal configuré, cela veut-il dire qu’un utilisateur suffisamment motivé pourrait faire en sorte que du contenu utilisateur soit uploadé dans Amazon Glacier, puis servi plus tard depuis là ?
https://docs.aws.amazon.com/service-authorization/latest/ref...
Les clés de condition se trouvent notamment ici, avec des clés permettant de contrôler l’accès selon la classe de stockage, les tags, etc.
https://docs.aws.amazon.com/service-authorization/latest/ref...