Radosław Miernik
Open source? Embrace, understand, develop.
Kurs Tworzenia Aplikacji Frontendowych 2022
Atakujący na swojej stronie osadza jakąś interesującą treść (np. "Darmowy iPhone 14!!!"), przez którą przechodzą kliknięcia (pointer-events: none
) i trafiają w <iframe>
z atakowaną aplikacją tam, gdzie atakujący chce (np. "Polub nasz szemrany post").
Można się bronić ustawiając konkretne nagłówki (Content-Security-Policy
) lub obchodząc to specjalnym kodem który sprawdzi, czy aplikacja jest osadzona (i np. zablokuje wtedy pewne operacje).
Skoro wykonujemy kod w czyjejś przeglądarce, to mamy dostęp do wszystkiego co ta nam oferuje, czyli możemy...
document.cookie
czy localStorage
.Rozróżniamy dwa (główne) rodzaje, zależne od tego, skąd bierze się wstrzyknięty kod.
Persistent (lub stored) XSS, gdzie złośliwy kod znajduje się w jakiś trwałym miejscu (np. serwerze).
Reflected XSS, gdzie złośliwy kod znajduje się w innym, nietrwałym miejscu (np. linku).
async function loadCommentText(
list: HTMLUListElement,
id: string,
) {
// Load comment from the API.
const url = `https://example.com/comments/${id}`;
const response = await fetch(url);
const { html } = await response.json();
// Create list item and add it to the list.
const node = document.createElement('li');
node.innerHTML = html;
list.appendChild(commentNode);
}
async function renderSearchHeader() {
// Get the search term from
const url = new URL(window.location);
const searchTerm = url.searchParams.get('search');
// Find the existing header.
const selector = '#search-header';
const header = document.querySelector(selector);
// Render it.
const html = `Results for <b>${searchTerm}</b>`;
header.innerHTML = html;
}
innerHTML
Zdaje się najczęstszym źródłem tej podatności jest brak sanityzacji ("czyszczenia") kodu HTML, który pochodzi od użytkownika. Ale są też inne sposoby:
Różne atrybuty DOM, np. href
w tagu <a>
czy src
i onerror
w tagu <img>
.
<a href="javascript:alert('Boom!')">
Login
</a>
<img src="https://invalid.url"
onerror="alert('Boom!');">
setTimeout("alert('Boom!')", 1000);
function defineUserColor(color: string) {
const style = document.createElement('style');
style.textContent = `
.user-color {
color: ${color};
}
`;
}
defineUserColor(`red;
background: url("https://ip.tracking.evil.com")
`);
Wszystko zależy od tego, jaką konkretnie podatność mamy. Podstawą natomiast powinien być bezwzględny zakaz ufania danym pochodzącym od użytkownika.
Przykładowo, jeżeli wyświetlamy jakąś treść, użyjmy textContent
zamiast innerHTML
. Jeżeli faktycznie potrzebujemy tam HTML (np. edytor komentarzy pozwala formatować tekst), to musimy go odpowiednio zabezpieczyć.
// There are tens of packages that sanitze the HTML.
// This is just an example!
import sanitizeHTML from 'sanitize-html';
const safeHTML = sanitizeHTML(unsafeHTML, {
// Other tags are stripped.
allowedTags: ['a', 'b', 'i'],
// Other attributes are stripped.
allowedAttributes: { a: ['href'] },
// The `href` tag has to start with `https://`.
// Other URL schemes are stripped entirely.
allowedSchemes: ['https'],
});
Jak można się domyśleć, najczęstszym celem tego ataku są serwery i atakujący jest dla nas stroną trzecią. Może się natomiast zdarzyć tak, że to my sami wykonamy taki "atak" na naszej aplikacji przypadkiem.
Jako przykład weźmy listę 8 (Pokédex). Wyobraźmy sobie, że chcielibyśmy wyświetlić obrazki nie tylko po kliknięciu ale już na samej liście.
Wymaga to od nas N+1
zapytań, gdzie N
to liczba Pokémonów. Pierwsze, pobierające listę, musi się zakończyć najpierw, natomiast pozostałe mogą być wykonane równolegle.
function renderPokemonList(limit: number) {
const api = 'https://pokeapi.co/api/v2/pokemon';
const url = `${api}?limit=${limit}`;
const response = await fetch(url);
const list = await response.json();
const listWithSprites = Promise.all(
list.map(async ({ name, url }) => {
const response = await fetch(url);
const { sprites } = await response.json();
return { sprites, name };
}),
);
listWithSprites.forEach(renderPokemon);
}
Przede wszystkim nasz serwer powinien sobie jakoś poradzić. Niezależnie od tego, czy to przez przypadek czy specjalnie, natłok równoległych zapytań nie powinien powodować problemów.
Jeżeli chodzi o sam frontend, to sama przeglądarka już trochę pomaga, bo ogranicza ilość równoległych zapytań. Możemy to kontrolować sami, kolejkując je odpowiednio.
Bardzo prostym i skutecznym rozwiązaniem jest paczka p-limit
, która ogranicza ilość równolegle uruchomionych Promise
ów.
Open Web Application Security Project to fundacja o jasnym celu: poprawa bezpieczeństwa oprogramowania.
To co jest dla nas najbardziej interesujące, to obszerne podsumowania, jak na przykład to: Cross Site Scripting Prevention Cheat Sheet.
Ich lista OWASP Top Ten jest praktycznie standardem jeżeli chodzi o audyty bezpieczeństwa i choć mało z nich dotyczy frontendu, to często jest on pomijany a w rezultacie bardzo podatny.
Fin
By Radosław Miernik
Wykład na Kurs Tworzenia Aplikacji Frontendowych (https://github.com/Arsenicro/uwr2022-frontend)