Créez en JavaScript votre fichier polyglotte
HTML / ZIP / PNG

Introduction

  • Développeur Web depuis 20+ ans
  • Auteur de projets open-source :
    • zip.js : bibliothèque pour lire et écrire des fichiers zip en JavaScript
    • SingleFile : extension et outil en ligne de commande pour sauver une page web dans un simple fichier HTML
      • utilisation de data URIs pour stocker les contenus binaires en base 64, par ex. data:;base64,aGVsbG8=
      • taille du fichier de sortie plus large que la taille de toutes les ressources additionnées (environ 33%)
  • Peut-on combiner SingleFile et zip.js pour produire des fichiers plus compacts et plus faciles à manipuler ?
  • Peut-on aller plus loin ?

Projet de test

  • Page HTML simple
  • Inclusion de ressources externes :
    • images : image.png, background.png
    • feuilles de style CSS : style.css, properties.css
    • script JavaScript : script.js
  • Feuilles de style et script encodés en UTF-8
  • Fichiers stockés dans le répertoire project/

Projet de test

Projet de test

Structure du projet

project/index.html
project/script.js
project/style.css
project/properties.css
project/image.png
project/background.png
							
						

Projet de test

Structure du projet

project/index.html
project/script.js
project/style.css
project/properties.css
project/image.png
project/background.png
							
						

Projet de test

Structure du projet

project/index.html
project/script.js
project/style.css
project/properties.css
project/image.png
project/background.png
							
						

Projet de test

Structure du projet

project/index.html
project/script.js
project/style.css
project/properties.css
project/image.png
project/background.png
							
						

Format ZIP

  • Créé en 1989 par Phil Katz chez PKWARE (éditeur de PKZIP)
  • Supporte :
    • la compression au format DEFLATE depuis la version 2.0 (1993)
    • le chiffrage AES depuis la version 5.2 (2003)
  • Version 2.0+ supportée sur la plupart des systèmes d'exploitation
  • Exemples de formats basés sur le format ZIP :
    • Documents LibreOffice/MS Office (.ODT, .DOCX, ...)
    • Archives Java (.JAR)
    • Packages Android (.APK et Archives iOS .IPA)
    • Web Extensions (.CRX et .XPI)

Format ZIP

  • Entrées (file entries) suivies de l'annuaire central (central directory)
  • Adapté pour la lecture/écriture en streaming mais avec quelques limitations en lecture
  • Certaines métadonnées sont stockées en double dans les en-têtes locaux (local headers) et l'annuaire central : nom du fichier, date de dernière modification, taille des données ...
  • Annuaire central est requis et fait autorité
  • Annuaire central contient les positions (relative offsets) de chacune des entrées dans le fichier ZIP
  • Les metadonnées des en-têtes locaux peuvent être utilisées pour réparer un fichier ZIP

Format ZIP

Source: https://en.wikipedia.org/wiki/ZIP_(file_format)

Format ZIP & JavaScript

Exemple avec zip.js

Exemple avec zip.js

Exemple de création d'un fichier ZIP

					import { ZipWriter, Uint8ArrayWriter } from "@zip-js/zip-js"

const zipDataWriter = new Uint8ArrayWriter()
const zipWriter = new ZipWriter(zipDataWriter)

for await (const { name } of readDirectory(inputFolder)) {
	const readableStream = await readFileStream(name)
	await zipWriter.add(name, readableStream)
}

await zipWriter.close()
const zipData = zipDataWriter.getData() // Uint8Array
console.log("zip file data:", zipData)
				

Intégration dans le projet

Intégration dans le projet

Création du fichier ZIP

index.html
index.js

lib/utils-zip.js
							
						

Intégration dans le projet

Création du fichier ZIP

index.html
index.js

lib/utils-zip.js
							
						

Intégration dans le projet

Création du fichier ZIP

index.html
index.js

lib/utils-zip.js
							
						

Intégration dans le projet

Création du fichier ZIP

index.html
index.js

lib/utils-zip.js
							
						

Exemple avec zip.js

Exemple avec zip.js

Exemple de lecture d'un fichier ZIP

					import { ZipReader, BlobReader, BlobWriter } from "@zip-js/zip-js"

const zipReader = new ZipReader(new BlobReader(blob))
const entries = await zipReader.getEntries()

for (const entry of entries) {
	const blob = await entry.getData(new BlobWriter())
	console.log("file:", entry.filename, "blob:", blob)
}

await zipReader.close()
			

Intégration dans le projet

Intégration dans le projet

Lecture du fichier ZIP

index.html
index.js
							
						

Intégration dans le projet

Lecture du fichier ZIP

index.html
index.js
							
						

Intégration dans le projet

Lecture du fichier ZIP

Format ZIP (suite)

  • Format extensible :
    • 64Ko de données après le fichier ZIP (commentaire)
    • Offset supérieur à zéro pour la première entrée dans le fichier zip
  • Possibilité d'ajouter du contenu avant et après un fichier ZIP tout en restant valide
  • Structure de page HTML auto-extractible :
    1. Contenu HTML jusqu'à une balise ouvrante <!--
    2. Contenu du fichier ZIP
    3. Balise fermante --> et fin du contenu HTML

Format ZIP (suite)

Template du fichier HTML auto-extractible

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Please wait...</title>
		<script><!-- Contenu de assets/zip.min.js --></script>
	</head>
	<body>
		<p>Please wait...</p>
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Format ZIP (suite)

Template du fichier HTML auto-extractible

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Please wait...</title>
		<script><!-- Contenu de assets/zip.min.js --></script>
	</head>
	<body>
		<p>Please wait...</p>
		<!-- Données ZIP     -->
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

1/4 - Extraction et affichage des entrées du fichier ZIP

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

2/4 - Affichage de la page index.html

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP

3/4 - Affichage de la page complète

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Code JavaScript inactif ⚠️

Fichier polyglotte HTML/ZIP

4/4 - Prise en charge des scripts

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture de la page depuis le système de fichier ⚠️

Lecture du fichier ZIP depuis le système de fichier

  • Contournement de l'appel à await fetch("")
  • Lecture du fichier ZIP depuis le DOM via Node#textContent
  • Corruption de données engendrée par l'encodage des caractères :
    • encodage des caractères sur 1 octet (e.g. windows-1252) contre plusieurs octets (e.g. UTF-8)
    • remplacement par un caractère de remplacement U+FFFD des caractères dont le code est 0 ou est invalide (suivant l'encodage)
    • remplacement par un passage à la ligne \n des caractères :
      • retours chariot \r
      • retours chariot suivis immédiatement d'un passage à la ligne \r\n
    • remplacement de certains caractères (suivant l'encodage) dont le code est supérieur à 127, i.e. au delà de la table ASCII 7-bit

Lecture du fichier ZIP depuis le DOM

Lecture du fichier ZIP depuis le DOM

Affichage en hexadécimal des données ZIP lues sous forme de texte

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

UTF-8 :

windows-1252 :

Lecture du fichier ZIP depuis le DOM

Comparaison de l'impact des différents encodages sur 1 octet

  • Pages web de test contenant un contenu binaire de 256 octets avec toutes les valeurs possibles
  • Test avec tous les encodages supportés
  • Lecture des donnés binaires sous forme de texte en JavaScript
  • Calcul et affichage du nombre de caractères :
    • égaux au caractère de remplacement U+FFFD (première colonne)
    • différents de ceux attendus (seconde colonne)

Lecture du fichier ZIP depuis le DOM

Comparaison de l'impact des différents encodages sur 1 octet

Lecture du fichier ZIP depuis le DOM

Evolutions dans le template HTML

  • Remplacement de l'encodage UTF-8 par windows-1252
  • Calcul des données consolidées permettant de restaurer le contenu binaire
  • Tableau de 2 tableaux contenant les index de tous les :
    • retours chariot \r
    • retours chariot suivis immédiatement d'un passage à la ligne \r\n
  • Insertion des données consolidées en JSON dans une balise <script>

Lecture du fichier ZIP depuis le DOM

Template du fichier HTML auto-extractible (avant)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Please wait...</title>
		<script><!-- Contenu de assets/zip.min.js --></script>
	</head>
	<body>
		<p>Please wait...</p><!--
		  Données ZIP 
		-->
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Lecture du fichier ZIP depuis le DOM

Template du fichier HTML auto-extractible (après)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="windows-1252">
		<title>Please wait...</title>
		<script><!-- Contenu de assets/zip.min.js --></script>
	</head>
	<body>
		<p>Please wait...</p><!--
		  Données ZIP 
		-->
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Lecture du fichier ZIP depuis le DOM

Template du fichier HTML auto-extractible (après)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="windows-1252">
		<title>Please wait...</title>
		<script><!-- Contenu de assets/zip.min.js --></script>
	</head>
	<body>
		<p>Please wait...</p><!--
		  Données ZIP 
		-->
		<script type="text/json"> Données de consolidation     </script>
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Lecture du fichier ZIP depuis le DOM

Prise en charge des données consolidées dans le template HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture du fichier ZIP depuis le DOM

Prise en charge des données consolidées dans le template HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture du fichier ZIP depuis le DOM

Ajout des données consolidées dans la page HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture du fichier ZIP depuis le DOM

Ajout des données consolidées dans la page HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture du fichier ZIP depuis le DOM

Ajout des données consolidées dans la page HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture du fichier ZIP depuis le DOM

Contournement de l'appel à await fetch("")

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Lecture du fichier ZIP depuis le DOM

Contournement de l'appel à await fetch("")

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Les ressources de la page sont décodées en windows-1252 ⚠️

Lecture du fichier ZIP depuis le DOM

Correction des problèmes de type MIME des ressources externes

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js

assets/main.js
							
						

Format PNG

  • Créé en 1996 par un groupe de travail public
  • Format d'image standard et libre de droits
  • Compression sans perte (DEFLATE)
  • Fichier composé d'une signature suivie d'une séquence de blocs (chunk)
  • Structure d'un bloc :
    1. Longueur des données du bloc (4 octets)
    2. Type du bloc (4 octets) : IHDR, IDAT, IEND, tEXt ...
    3. Données du bloc (longueur variable)
    4. Code redondant cyclique (CRC32) calculé à partir de l'ensemble des données du bloc

Format PNG

  • Structure minimale d'un fichier PNG :
    1. Signature PNG (8 octets)
      89 50 4E 47  0D 0A 1A 0A
    2. Bloc IHDR pour l'en-tête (13 octets)
      00 00 00 0D  49 48 44 52  ...
    3. Bloc(s) IDAT pour les données de l'image
    4. Bloc IEND pour la fin des données (12 octets)
      00 00 00 00  49 45 4E 44  AE 42 60 82
  • Bloc tEXt pour stocker des données textes ou binaires
    xx xx xx xx  74 45 58 74  ...

Format PNG

Structure minimale d'un fichier PNG


Type de données Données en hexadécimal Obligatoire Taille (octets)
Signature PNG 89 50 4E 47 0D 0A 1A 0A 8
Bloc d'en tête IHDR 00 00 00 0D 49 48 44 52 ... 13
...
Bloc de données IDAT xx xx xx xx 49 44 41 54 ... 12 + n
...
Bloc de fin IEND 00 00 00 00 49 45 4E 44 AE 42 60 82 12

Fichier polyglotte HTML/ZIP/PNG

  • Encapsulation du fichier HTML/ZIP dans un fichier PNG
  • Structure de fichier polyglotte PNG :
    1. Signature PNG
    2. Bloc d'en-tête PNG
    3. Bloc de texte PNG : contenu HTML jusqu'à la balise ouvrante <!--
    4. Bloc(s) de données PNG
    5. Bloc de texte PNG : balise fermante --> et fin du contenu HTML
    6. Bloc de fin PNG

Fichier polyglotte HTML/ZIP/PNG

Structure du fichier PNG (avant)


Type de données Données en hexadécimal Obligatoire Taille (octets)
Signature PNG 89 50 4E 47 0D 0A 1A 0A 8
Bloc d'en tête IHDR 00 00 00 0D 49 48 44 52 ... 13
...
Bloc de données IDAT xx xx xx xx 49 44 41 54 ... 12 + n
...
Bloc de fin IEND 00 00 00 00 49 45 4E 44 AE 42 60 82 12

Fichier polyglotte HTML/ZIP/PNG

Structure du fichier PNG (après)


Type de données Données en hexadécimal Obligatoire Taille (octets)
Signature PNG 89 50 4E 47 0D 0A 1A 0A 8
Bloc d'en tête IHDR 00 00 00 0D 49 48 44 52 ... 13
Bloc de texte tEXt xx xx xx xx 74 45 58 74 ... 12 + n
...
Bloc de données IDAT xx xx xx xx 49 44 41 54 ... 12 + n
...
Bloc de texte tEXt xx xx xx xx 74 45 58 74 ... 12 + n
Bloc de fin IEND 00 00 00 00 49 45 4E 44 AE 42 60 82 12

Fichier polyglotte HTML/ZIP/PNG

Template du fichier HTML auto-extractible (avant)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="windows-1252">
		...
	</head>
	<body>
		<p>Please wait...</p><!--      Données ZIP 
		--><script type="text/json">
		  Données de consolidation
		</script>
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Fichier polyglotte HTML/ZIP/PNG

Template du fichier HTML auto-extractible (après)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="windows-1252">
		...
	</head>
	<body>
		<p>Please wait...</p><!-- Bloc(s) IDAT     --><!--
		  Données ZIP
		--><script type="text/json">
		  Données de consolidation
		</script>
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>

Fichier polyglotte HTML/ZIP/PNG

Template du fichier HTML auto-extractible (après)

Signature PNG + Bloc IHDR (21 octets)
<!DOCTYPE html>
<html>
	<head>
		<meta charset="windows-1252">
		...
	</head>
	<body>
		<p>Please wait...</p><!-- Bloc(s) IDAT     --><!--
		  Données ZIP 
		--><script type="text/json">
		  Données de consolidation
		</script>
		<script><!-- Contenu de assets/main.js --></script>
  </body>
</html>
Bloc IEND (12 octets)

Fichier polyglotte HTML/ZIP/PNG

Encapsulation du fichier HTML/ZIP dans un fichier PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP/PNG

Encapsulation du fichier HTML/ZIP dans un fichier PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP/PNG

Encapsulation du fichier HTML/ZIP dans un fichier PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP/PNG

Encapsulation du fichier HTML/ZIP dans un fichier PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP/PNG

Encapsulation du fichier HTML/ZIP dans un fichier PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

Fichier polyglotte HTML/ZIP/PNG

Encapsulation du fichier HTML/ZIP dans un fichier PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

Des noeuds textes induits par le format PNG sont visibles ⚠️

Fichier polyglotte HTML/ZIP/PNG

Suppression des nœuds textes induits par le format PNG

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

La page est rendue en mode quirks ⚠️

Fichier polyglotte HTML/ZIP/PNG

Correction du mode de rendu de la page HTML et du chargement des scripts

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js
							
						

L'image principale est stockée en double dans le fichier 🤔

Fichier polyglotte HTML/ZIP/PNG

Réutilisation de l'image dans la page HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js

project/index.html
							
						

Fichier polyglotte HTML/ZIP/PNG

Réutilisation de l'image dans la page HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js

project/index.html
							
						

Fichier polyglotte HTML/ZIP/PNG

Réutilisation de l'image dans la page HTML

index.html
index.js

lib/html-template.js
lib/utils.js
lib/utils-zip.js
lib/utils-png.js

assets/main.js

project/index.html
							
						

Conclusion

  • Limitations de l'implémentation finale :
    • Résolution des dépendances faite manuellement
    • Dépassement de la limite de 64Ko de données après le fichier ZIP (commentaire)
    • Présence de --> dans les données binaires ZIP ou PNG
    • Utilisation de String#replaceAll() pour remplacer les chemins dans les fichiers texte au lieu de s'appuyer sur une analyse syntaxique
    • Absence de balise <meta> contenant la Content Security Policy (CSP)
    • Non prise en charge des frames
    • ...
  • Formats alternatifs : MHTML, Web Bundle, WARC/WACZ, MAFF ...
  • Est-ce dangereux ? 🤷 (GIFAR)

Merci !

Questions ? 🤔, Feedback ? 📝