Onbeperkte bestandsupload
CWE-434CWE-400OWASP A04:2021Bijgewerkt 3 september 20265 min leestijd
Een uploadfunctie die elk bestandstype accepteert en het resultaat binnen de webroot bewaart, kan een aanvaller de mogelijkheid geven code op uw server uit te voeren. Controle op de bestandsnaam of het opgegeven content-type is onvoldoende, omdat de gebruiker beide zelf bepaalt. Wat telt is waar het bestand belandt en hoe het wordt teruggegeven.
Een uploadfunctie is een van de weinige plaatsen waar u een gebruiker toestaat een bestand op uw server te zetten. Alles hangt daarna af van twee vragen: wat er precies is opgeslagen, en wat de server ermee doet als iemand het opvraagt. Hieronder leest u waarom de gebruikelijke controles op naam en type onvoldoende zijn, en welke maatregelen wél het verschil maken.
Wat is een onbeperkte bestandsupload?
Van een onbeperkte bestandsupload spreken we wanneer een applicatie bestanden accepteert zonder afdoende te controleren wat er wordt aangeleverd, en die bestanden vervolgens zo opslaat dat ze door de webserver kunnen worden verwerkt. De ernst zit zelden in het uploaden zelf; die zit in de combinatie met de opslaglocatie en de manier waarop het bestand later wordt teruggegeven.
Het onderscheid is belangrijk, want het bepaalt waar de oplossing zit. Een PHP-bestand dat op een schijf staat waar de webserver niets mee doet, is een onschuldig tekstbestand. Datzelfde bestand in een map die door de webserver wordt uitgeleverd, is een afstandsbediening voor uw server.
Denk aan een postkamer die pakketten aanneemt. Het probleem is niet dat er een pakket binnenkomt; het probleem is dat het ongeopend wordt doorgezet naar een ruimte waar de inhoud automatisch in werking treedt. De controle hoort te gebeuren vóórdat het pakket op die plek belandt, en de plek zelf hoort zo gekozen te zijn dat er niets vanzelf in werking kán treden.
Hoe gaat een bestandsupload mis?
Kwetsbaar:
const multer = require('multer');
// Bewaart onder de door de gebruiker opgegeven naam, in de webroot
const upload = multer({ dest: './public/uploads' });
app.post('/profiel/foto', upload.single('bestand'), (req, res) => {
const doel = path.join('./public/uploads', req.file.originalname);
if (req.file.mimetype.startsWith('image/')) { // door de client bepaald
fs.renameSync(req.file.path, doel);
return res.send('Geüpload naar /uploads/' + req.file.originalname);
}
res.status(400).send('Alleen afbeeldingen');
});
Er zijn hier drie fouten die elkaar versterken. De controle gebeurt op mimetype, een waarde die de client zelf meestuurt en dus vrij te kiezen is. De bestandsnaam komt ongewijzigd van de gebruiker. Ook het resultaat belandt in public/, waar de webserver het uitlevert. Een aanvaller stuurt:
POST /profiel/foto HTTP/1.1
Content-Type: multipart/form-data; boundary=x
--x
Content-Disposition: form-data; name="bestand"; filename="shell.php"
Content-Type: image/png
<?php system($_GET['c']); ?>
--x--
Het opgegeven type is image/png, dus de controle laat het door. Het bestand komt terecht op /uploads/shell.php, en een verzoek naar https://portaal.example/uploads/shell.php?c=whoami voert commando’s uit op uw server. Van een profielfoto naar volledige controle in één verzoek.
Veilig:
const TOEGESTAAN = new Map([
['image/jpeg', 'jpg'],
['image/png', 'png'],
['application/pdf', 'pdf'],
]);
const upload = multer({
dest: '/var/opslag/tijdelijk', // buiten de webroot
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
});
app.post('/profiel/foto', upload.single('bestand'), async (req, res) => {
// Type bepalen uit de werkelijke inhoud, niet uit wat de client beweert
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');
}
// Naam van de gebruiker wordt volledig weggegooid
const naam = `${randomUUID()}.${extensie}`;
await fs.promises.rename(req.file.path, `/var/opslag/uploads/${naam}`);
await bestanden.registreer({ naam, eigenaar: req.gebruiker.id, type: type.mime });
res.json({ id: naam });
});
// Uitleveren via een route die autoriseert en niets laat uitvoeren
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/uploads/${bestand.naam}`);
});
De verdediging bestaat hier uit lagen die elk afzonderlijk de aanval al breken. Het type wordt bepaald uit de werkelijke inhoud van het bestand. De naam van de gebruiker wordt volledig vervangen door een willekeurige waarde, wat meteen path traversal en overschrijven uitsluit. De opslag ligt buiten de webroot, dus de webserver kán het bestand niet uitvoeren. En bij het uitleveren wordt geautoriseerd, wordt sniffing uitgeschakeld en wordt het bestand als download aangeboden.
Content-Type in een upload zijn volledig door de aanvaller te bepalen. De enige betrouwbare bronnen zijn de werkelijke inhoud van het bestand en de plek waar u het neerzet.Wat is de impact van een onbeperkte bestandsupload?
De ernst is hoog tot kritiek, en dat maakt dit een van de zwaardere bevindingen in een webapplicatie. De reden is de directe route naar code-uitvoering op de server.
Slaagt een aanvaller erin een uitvoerbaar bestand te uploaden op een plek die de webserver verwerkt, dan is de uitkomst volledige controle over de applicatieserver: het uitlezen van de database, het benaderen van configuratiebestanden met inloggegevens, en een uitvalsbasis voor verder onderzoek van het interne netwerk. Vanaf dat punt is de vraag niet meer wat er met de applicatie kan gebeuren, maar wat er met de infrastructuur kan gebeuren.
Ook zonder code-uitvoering blijven er reële gevolgen. Een bestand dat als HTML wordt teruggegeven levert cross-site scripting op uw eigen domein op. Een ongelimiteerde bestandsgrootte maakt het mogelijk uw opslag vol te schrijven. Een bestandsnaam met padtekens kan bestaande bestanden overschrijven. Verder uw platform kan worden misbruikt voor het verspreiden van malware, waarbij het uw domeinnaam is die de geloofwaardigheid levert.
Hoe spoor je een onbeperkte bestandsupload op?
Een tester begint met het vaststellen welke controles er zijn: wordt een bestand geweigerd op basis van de extensie, van het opgegeven content-type, of van de inhoud? Dat wordt uitgeprobeerd door een onschuldig ogende afbeelding aan te bieden met een gewijzigde naam, en omgekeerd een uitvoerbaar bestand met een afbeeldingstype.
Vervolgens draait het om de vraag waar het bestand terechtkomt en of het bereikbaar is. Kan de URL worden achterhaald of geraden? Wordt de oorspronkelijke naam behouden? Kan met padtekens in de naam buiten de doelmap worden geschreven? Wordt het bestand teruggegeven met een content-type dat is afgeleid van de naam? Ook worden de klassieke omwegen getest: dubbele extensies, hoofdlettervarianten, extensies die op de server wél maar in de controlelijst niet voorkomen, en bestanden die tegelijk een geldige afbeelding en geldige code zijn. AssistSec beoordeelt daarbij nadrukkelijk de uitleverroute, omdat een strenge uploadcontrole weinig waard is wanneer de bestanden vanaf hetzelfde domein en zonder autorisatie worden geserveerd.
Hoe voorkom je een onbeperkte bestandsupload?
- Werk met een toegestane lijst van bestandstypen en bepaal het type uit de werkelijke inhoud, niet uit naam of header.
- Vervang de door de gebruiker opgegeven bestandsnaam volledig door een zelf gegenereerde waarde.
- Sla uploads op buiten de webroot, of in objectopslag zonder uitvoeringsmogelijkheid.
- Lever bestanden uit via een route die autoriseert, in plaats van de map rechtstreeks open te stellen.
- Stuur bij het uitleveren
X-Content-Type-Options: nosniffenContent-Disposition: attachmentmee. - Serveer door gebruikers aangeleverde bestanden bij voorkeur vanaf een apart domein.
- Begrens de bestandsgrootte, het aantal bestanden en het totale opslagvolume per gebruiker.
- Verwerk afbeeldingen opnieuw (bijvoorbeeld door ze te herschalen), waarmee ingebedde code verdwijnt.
- Zet uitvoering van scripts uit in de uploadmap, als extra laag bovenop de opslag buiten de webroot.
- Scan uploads met een virusscanner als aanvulling, niet als basis.
Bronnen
Veelgestelde vragen
Is controleren op de extensie niet genoeg?
Nee, om twee redenen. De gebruiker bepaalt de bestandsnaam, dus die zegt niets over de inhoud. En afhankelijk van de serverconfiguratie kunnen namen als bestand.php.jpg of bestand.php%00.jpg alsnog als code worden uitgevoerd. Werk met een toegestane lijst én controleer de werkelijke inhoud.
Kan ik het content-type van de browser vertrouwen?
Nee. De header Content-Type in een upload wordt door de client meegestuurd en is volledig door de aanvaller te bepalen. Hij kan een uitvoerbaar bestand aanbieden met het type image/png. Bepaal het type zelf aan serverzijde op basis van de daadwerkelijke inhoud.
Waarom is opslaan buiten de webroot zo belangrijk?
Omdat een bestand dat niet via een URL bereikbaar is, ook niet door de webserver kan worden uitgevoerd. Zelfs als een aanvaller erin slaagt een scriptbestand te uploaden, kan hij het dan niet aanroepen. Serveer bestanden via een route die ze uitleest en doorstuurt, niet door de map open te stellen.
Helpt een virusscanner?
Als extra laag, ja, maar hij is geen basis. Een scanner herkent bekende schadelijke bestanden en mist gerichte of nieuwe varianten. Bovendien lost hij het kernprobleem niet op: een volkomen schoon PHP-bestand is geen virus, maar wel fataal als de server het uitvoert.
Verwante artikelen
- 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.
- KwetsbaarhedenCWE-22A01:2021Path traversal (directory traversal)Path traversal (directory traversal) uitgelegd: hoe aanvallers met ../ uit de bedoelde map breken en gevoelige serverbestanden lezen, en hoe u het voorkomt.
- KwetsbaarhedenCWE-94A03:2021Remote code execution (RCE)Remote code execution (RCE) uitgelegd: hoe aanvallers via ongefilterde invoer eigen commando's of code op uw server draaien, en hoe u het voorkomt.
- KwetsbaarhedenCWE-79A03:2021Cross-site scripting via bestandsuploadEen geüploade SVG of HTML die vanaf uw eigen domein wordt getoond, voert scriptcode uit binnen uw origin. Lees hoe u dat afsluit.