Zip Slip in xslweb: een ontbrekende padcheck in een ingebouwde unzip-functie
De ingebouwde unzip-functie van het Java XSLT-framework xslweb extraheerde ZIP-bestanden zonder te controleren of een entry buiten de doelmap schreef. Die fout reisde sinds 2015 mee in elke release en is inmiddels gefixed door de maintainer. Dit is de technische anatomie van de bug, de fix, en waarom dit patroon zo vaak terugkomt.
Wat is xslweb?
xslweb is een open source webframework voor ontwikkelaars die liever in XSLT en XQuery werken dan in een traditionele taal. Een applicatie bestaat uit stylesheets die een XML-representatie van het HTTP-request omzetten naar een XML-representatie van de response. Het framework levert daar een bibliotheek van XPath- en XQuery-extensiefuncties bij: HTTP-aanroepen, database-toegang, bestandstoegang, en ook een functie om een ZIP-bestand uit te pakken. Die laatste, xslweb:unzip($source, $target), staat centraal in dit artikel.
Het is geen bekende naam in de Java-wereld, maar xslweb wordt sinds 2015 onderhouden en draait, net als veel vergelijkbare frameworks, als WAR-bestand in een servlet-container zoals Tomcat.
Zip Slip in een notendop
Zip Slip is een kwetsbaarheidsklasse die Snyk in 2018 voor het eerst breed documenteerde, nadat het bedrijf hem in tientallen populaire Java-, JavaScript-, Go- en .NET-projecten had aangetroffen. De kern van het probleem: een ZIP-bestand mag een entry-naam bevatten zoals ../../../etc/cron.d/evil. Plakt de code die het archief uitpakt die naam blind achter een doelmap, dan schrijft hij het bestand niet in die map, maar ergens daarbuiten, overal waar het proces dat de unzip uitvoert toevallig schrijfrechten heeft.
Acht jaar na dat onderzoek duikt exact dezelfde fout nog steeds op, en xslweb is het nieuwste voorbeeld dat ik tegenkwam.
Waar het misging
De functie
xslweb biedt stylesheets de extensiefunctie xslweb:unzip($source, $target) aan, gedefinieerd in Unzip.java. $source mag een lokaal bestandspad of een file:-URI zijn. Interessant genoeg accepteert de functie ook een http(s)://-URL: in dat geval haalt xslweb het bestand zelf op. $target is de map waarin de inhoud terechtkomt. De daadwerkelijke uitpaklogica zit in ZipUtils.unzipStream(), en zag er vóór de fix zo uit:
while ((entry = zis.getNextEntry()) != null) {
File file = new File(extractTo, entry.getName());
if (entry.isDirectory()) {
if (!file.exists()) {
file.mkdirs();
}
} else {
...
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
...
}
}
new File(extractTo, entry.getName()) is de hele kwetsbaarheid in één regel. entry.getName() komt rechtstreeks uit het ZIP-bestand en wordt nergens genormaliseerd of gecontroleerd. Een entry met naam ../../../../opt/tomcat/webapps/ROOT/shell.jsp wordt keurig naar precies dat pad geschreven, ver buiten extractTo.
Waarom dit uitvoerbaar is, niet alleen theoretisch
De functie is bedoeld voor ontwikkelaars die bijvoorbeeld een door een bezoeker geüploade ZIP willen uitpakken naar een werkmap, zoals een themapakket, een content-import of een set afbeeldingen:
<xsl:value-of select="xslweb:unzip($uploaded-zip-path, $target-dir)"/>
De ontwikkelaar vertrouwt erop dat xslweb:unzip() dat veilig doet. Dat is precies waarvoor je een frameworkfunctie gebruikt in plaats van het zelf te bouwen. Maar de functie deed geen enkele validatie. Elke xslweb-applicatie die deze ingebouwde functie gebruikte om door bezoekers aangeleverde ZIP-content uit te pakken, erfde de kwetsbaarheid automatisch: niet doordat de ontwikkelaar een fout maakte, maar doordat de bouwsteen die het framework aanbood er zelf een bevatte.
Een tweede observatie: $source accepteert ook een URL die de server zelf ophaalt. Een stylesheet die een aanvrager-gestuurde parameter ongefilterd doorgeeft aan xslweb:unzip() combineert Zip Slip met SSRF: de server haalt dan een ZIP op van een locatie die de aanvaller controleert, en pakt die vervolgens onveilig uit.
Het effect
Wat een aanvaller precies kan overschrijven, hangt af van de deployment: de schrijfrechten van het servlet-containerproces, de directorystructuur, welke bestanden de applicatie zelf weer inleest. Maar het patroon is bekend: een JSP-bestand droppen in de webapps-map van Tomcat levert doorgaans remote code execution op. Op zijn minst geeft willekeurige bestandsschrijftoegang vrijwel altijd een pad naar verdere escalatie, zoals het overschrijven van configuratiebestanden, het vervangen van stylesheets of het manipuleren van logbestanden.
De fix
Maarten Kroon, de maintainer van xslweb, mergede op 26 mei 2026 een herschreven unzipStream() (commit 518c9ed). De kern van de fix is een validatiestap die vóór elke schrijfactie wordt uitgevoerd:
private static Path resolveEntryPath(Path destRoot, ZipEntry entry) throws IOException {
// Normalize ensures that any ../ segments are removed before validation
Path resolved = destRoot.resolve(entry.getName()).normalize();
if (!resolved.startsWith(destRoot)) {
throw new IOException("Zip Slip detected for entry: " + entry.getName());
}
return resolved;
}
Elke entry wordt eerst opgelost tegen de doelmap en genormaliseerd, waardoor ../-segmenten verdwijnen, en pas dan gecontroleerd: ligt het resultaat nog steeds onder de doelmap? Zo niet, dan gooit de functie een exception in plaats van te schrijven. De rest van de implementatie is overgezet naar de java.nio.file-API's, met try-with-resources en een expliciete fout bij een leeg archief.
Gevonden en gemeld door: Sofyan Aarrass (Resync).
Fix: gemerged op 26 mei 2026 door maintainer Maarten Kroon (commit 518c9ed).
Gepubliceerde release met de fix: nog geen. De laatste getagde release is v4.2.0 (januari 2022).
CVE: aanvraag ingediend op 25 juni 2026; toekenning loopt op het moment van schrijven nog. Dit artikel wordt bijgewerkt zodra er een ID is.
Elke gepubliceerde release, inclusief de huidige v4.2.0, bevat de kwetsbare code. Er is simpelweg nog geen release uitgebracht ná de fix. Bouwt u zelf vanaf de laatste master-branch, dan krijgt u de patch mee. Gebruikt u een gepubliceerde release en verwerkt uw applicatie ZIP-bestanden van bezoekers via xslweb:unzip(), overweeg dan om de validatie uit de fix hierboven lokaal toe te passen totdat een nieuwe release verschijnt.
Een kwetsbaarheid die al tien jaar meereist
Het is de moeite waard om stil te staan bij hoe lang dit onopgemerkt bleef. unzipStream() verscheen in november 2015. Ik heb het nagetrokken: elke gepubliceerde release sindsdien, van v2.0.0 tot en met de huidige v4.2.0, bevat de kwetsbare versie. Niet omdat niemand naar de code keek, maar omdat een onveilige unzip-implementatie niet opvalt totdat iemand er specifiek naar zoekt: hij compileert, hij werkt, het happy-path-scenario slaagt. Zip Slip zit niet in de minderheid van de code die zelden wordt gebruikt. Hij zit in de meerderheid van de code die nooit met kwaadaardige input wordt getest.
De bredere les
Zip Slip is geen exotische kwetsbaarheid. Het patroon new File(parent, entry.getName()) staat in talloze tutorials en Stack Overflow-antwoorden, en niet toevallig ook in de trainingsdata van elke AI-codeassistent. (Hier raakt dit aan iets wat we ook beschrijven bij vibe-coded apps: code die werkt, wordt overal gekopieerd; code die specifiek veilig is, moet je er expliciet bovenop bouwen.) Een ZIP uitpakken is in praktisch elke taal en bibliotheek een functie die "gewoon werkt", zonder ingebouwde padbescherming. Dat moet u er zelf aan toevoegen, of een bibliotheek gebruiken die het al doet.
Hoe u dit zelf voorkomt
Vertrouw nooit blind op entry.getName()
Resolve het pad tegen de doelmap, normaliseer het, en controleer containment vóórdat u schrijft. Dat is precies het patroon van de fix hierboven: drie regels code die de hele kwetsbaarheidsklasse elimineren.
Of gebruik een bibliotheek die het al afdwingt
Apache Commons Compress en recentere versies van zip4j bieden veilige extractie-varianten. Kunt u die gebruiken in plaats van een handgeschreven uitpak-loop, doe dat dan.
Behandel "uitpakken" als onvertrouwde input, ook als de aanroep vertrouwd is
De kwetsbaarheid zat niet in wie de functie mocht aanroepen: dat was een vertrouwde partij, een ontwikkelaar die een stylesheet schrijft. Ze zat in de inhoud van het bestand dat werd uitgepakt. Of die inhoud nu door een bezoeker is geüpload of van een URL is opgehaald: zodra uw applicatie een ZIP, TAR of vergelijkbaar archief van buiten verwerkt, is die inhoud onvertrouwd, ongeacht hoe vertrouwd de code is die de verwerking aanroept.
Conclusie
Een unzip-functie die er al tien jaar staat, in een framework dat nog steeds in productie draait, met een fout die in drie regels te fixen blijkt. Dat is precies waarom Zip Slip, jaren na het oorspronkelijke onderzoek van Snyk, nog steeds opduikt: het is geen ingewikkelde kwetsbaarheid om te vinden. Het is een kwetsbaarheid die niemand controleert, totdat iemand het wel doet.
Voor xslweb: gefixed in code, nog niet in een release, CVE in aanvraag. Voor de rest van ons: een goede reden om elke "uitpakken"-functie in uw eigen stack één keer kritisch te bekijken.
Dit soort bugs vindt u niet met een scanner.
Padvalidatie, autorisatie, businesslogica: precies het soort kwetsbaarheid dat een handmatige pentest blootlegt en een geautomatiseerde scan overslaat.
Naar webapplicatie-pentest →