XSS apsauga moderniais JavaScript framework

Kodėl XSS vis dar yra problema 2024-aisiais

Galvojate, kad Cross-Site Scripting (XSS) atakos jau seniai turėjo tapti praeities reliktu? Deja, realybė kitokia. Net ir šiandien, kai naudojame pažangiausius JavaScript framework’us, XSS išlieka viena iš dažniausių saugumo spragų. OWASP Top 10 sąraše ji vis dar užima aukštą vietą, o tai reiškia, kad daugelis programuotojų arba nesupranta grėsmės, arba tiesiog neskiria pakankamai dėmesio.

Problema ta, kad modernūs framework’ai suteikia klaidingą saugumo jausmą. Daug kas mano, kad React, Vue ar Angular automatiškai apsaugo nuo visų XSS atakų. Iš dalies tai tiesa – šie įrankiai turi integruotus apsaugos mechanizmus, bet jie nėra stebuklingi. Viena klaida kode, vienas neteisingai panaudotas metodas, ir jūsų aplikacija tampa pažeidžiama.

Kas gi iš tikrųjų vyksta? XSS ataka leidžia piktavaliui įterpti kenkėjišką JavaScript kodą į jūsų svetainę. Kai kiti vartotojai atidaro tą puslapį, tas kodas vykdomas jų naršyklėje. Rezultatas? Pavogti prisijungimo duomenys, sesijos token’ai, asmens informacija ar net pilnas paskyros perėmimas.

Kaip React gina nuo XSS ir kur jis palieka spragų

React’as yra vienas populiariausių framework’ų, ir jo kūrėjai tikrai pagalvojo apie saugumą. Pagrindinis apsaugos mechanizmas – automatinis escape’inimas. Kai rašote JSX kodą ir įterpiame kintamąjį į HTML, React automatiškai pakeičia pavojingus simbolius saugiais ekvivalentais.

Pavyzdžiui, jei turite:

const userInput = '<script>alert("XSS")</script>';
return <div>{userInput}</div>;

React automatiškai escape’ins šį tekstą, ir naršyklė jį atvaizduos kaip paprastą tekstą, o ne vykdomą kodą. Tai puiku ir veikia 90% atvejų.

Bet štai kur prasideda problemos. React palieka „išėjimo liuką” – dangerouslySetInnerHTML. Šis metodas egzistuoja dėl priežasties (kartais tikrai reikia įterpti HTML), bet jo pavadinimas ne juokais įspėja apie pavojų. Jei naudojate šį metodą su nepatikrintais duomenimis, atidarote duris XSS atakoms.

// BLOGAI - pažeidžiama XSS
const Comment = ({ text }) => {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
};

Kita problema – HTML atributai. React apsaugo turinį, bet ne visus atributus. Pavyzdžiui, href atributas gali būti pavojingas:

// Pavojinga!
const Link = ({ url }) => {
  return <a href={url}>Spausk čia</a>;
};

Jei url kintamasis būtų javascript:alert('XSS'), turite problemą. Piktavalis gali įterpti ne tik javascript: protokolą, bet ir data: URL su base64 užkoduotu kodu.

Vue.js ir jo dvipusis duomenų surišimas

Vue.js turi panašų apsaugos modelį kaip React, bet su keliais skirtumais. Vue taip pat automatiškai escape’ina tekstinį turinį, kai naudojate dvigubus riestus skliaustus {{ }} arba v-text direktyvą.

Problema atsiranda su v-html direktyva – tai Vue ekvivalentas React’o dangerouslySetInnerHTML. Ir vėlgi, jei naudojate ją su vartotojo įvestimi, esate pažeidžiami.

<!-- BLOGAI -->
<div v-html="userComment"></div>

Vue dokumentacija aiškiai įspėja, kad v-html niekada neturėtų būti naudojama su nepatikimais duomenimis. Bet praktikoje matau šią klaidą nuolat – ypač kai programuotojai nori atvaizduoti rich text turinį iš duomenų bazės.

Dar viena Vue specifinė problema – dinamiški komponentai ir event handler’iai. Jei leidžiate vartotojui kontroliuoti, kuris komponentas bus atvaizduotas arba kokie event handler’iai bus priskirti, galite netyčia sukurti XSS spragą:

<!-- Pavojinga, jei componentName ateina iš vartotojo -->
<component :is="componentName"></component>

Angular ir jo griežtesnis požiūris

Angular’as iš visų trijų pagrindinių framework’ų turi griežčiausią požiūrį į saugumą. Jis naudoja DomSanitizer servisą, kuris automatiškai tikrina ir valo visus duomenis prieš juos atvaizduojant.

Angular’as skirsto turinį į keturias kategorijas:
– HTML turinys
– Stiliai (CSS)
– URL adresai
– Resource URL (script, iframe src ir pan.)

Kiekviena kategorija turi skirtingus saugumo reikalavimus. Kai bandote įterpti nepatikrintą turinį, Angular’as automatiškai jį blokuoja ir meta klaidą konsolėje. Tai geras dalykas – geriau matyti klaidą development’e nei saugumo spragą production’e.

Bet ir čia yra būdas apeiti apsaugą – bypassSecurityTrust... metodai:

// Pavojinga praktika
constructor(private sanitizer: DomSanitizer) {}

getHtml(value: string) {
  return this.sanitizer.bypassSecurityTrustHtml(value);
}

Šie metodai egzistuoja teisėtais tikslais, bet juos naudojant su vartotojo įvestimi, sukuriate XSS spragą. Angular’o filosofija – jei jums reikia bypass’inti saugumą, turėtumėte labai gerai pagalvoti, kodėl.

Praktiniai apsaugos būdai ir bibliotekos

Gerai, dabar kai suprantame problemas, pažiūrėkime, kaip jas spręsti praktiškai. Pirmas ir svarbiausias taisyklė – niekada nepasitikėkite vartotojo įvestimi. Net jei duomenys ateina iš jūsų duomenų bazės, jie kadaise galėjo būti įvesti vartotojo.

Vienas geriausių sprendimų – naudoti specialias sanitization bibliotekas. DOMPurify yra aukso standartas šioje srityje. Ji veikia su visais framework’ais ir labai gerai valo HTML turinį:

import DOMPurify from 'dompurify';

// React pavyzdys
const SafeComment = ({ html }) => {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
};

// Vue pavyzdys
const cleanHtml = computed(() => {
  return DOMPurify.sanitize(props.userHtml);
});

DOMPurify yra galingas, nes jis supranta HTML struktūrą ir gali pašalinti pavojingus elementus bei atributus, išlaikydamas saugų formatavimą. Jis palaiko konfigūraciją, leidžiančią tiksliai nurodyti, kas leidžiama, o kas ne.

Kitas svarbus aspektas – Content Security Policy (CSP) antraštės. Tai naršyklės lygio apsauga, kuri riboja, iš kur gali būti įkeltas JavaScript kodas:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; object-src 'none';

CSP neapsaugo nuo visų XSS atakų, bet labai sumažina jų poveikį. Net jei piktavaliui pavyktų įterpti kodą, CSP užkirs kelią jo vykdymui.

Input validacija ir output encoding

Yra dvi pagrindinės gynybos linijos prieš XSS: input validacija ir output encoding. Daugelis programuotojų sutelkia dėmesį tik į vieną iš jų, bet reikia abiejų.

Input validacija reiškia, kad tikrinate duomenis, kai jie ateina į sistemą. Jei laukiate el. pašto adreso, patikrinkite, ar tai tikrai atrodo kaip el. paštas. Jei laukiate skaičiaus, įsitikinkite, kad tai skaičius. Naudokite bibliotekas kaip Joi, Yup ar Zod:

import { z } from 'zod';

const userSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  bio: z.string().max(500)
});

// Validuojame prieš išsaugant
try {
  const validData = userSchema.parse(userData);
  // Saugome tik validuotus duomenis
} catch (error) {
  // Apdorojame klaidas
}

Output encoding vyksta, kai atvaizduojate duomenis. Kaip minėjau anksčiau, framework’ai dažniausiai tai daro automatiškai, bet ne visada. Svarbu suprasti kontekstą – ar įterpiame į HTML turinį, atributą, JavaScript kintamąjį ar CSS?

Pavyzdžiui, jei įterpiame duomenis į JSON objektą, kuris vėliau bus naudojamas JavaScript’e, reikia kitokio encoding’o nei HTML kontekste:

// BLOGAI - pažeidžiama
const config = `
  <script>
    const userData = ${JSON.stringify(userInput)};
  </script>
`;

// GERIAU - naudokite data atributus
<div id="app" data-user='${JSON.stringify(userInput).replace(/'/g, "&#x27;")}'></div>

// GERIAUSIAI - perduokite per framework'o mechanizmus
const App = () => {
  const [userData] = useState(initialData);
  // Framework'as pasirūpins saugumu
};

Server-Side Rendering ir nauji iššūkiai

Su SSR (Server-Side Rendering) ir framework’ais kaip Next.js, Nuxt.js ar SvelteKit atsiranda papildomų saugumo aspektų. Serverio pusėje generuojamas HTML turi būti toks pat saugus kaip ir kliento pusėje.

Viena dažna klaida – manyti, kad serverio pusėje duomenys yra saugūs. Bet jei tie duomenys ateina iš duomenų bazės, kurioje saugoma vartotojų įvestis, problema išlieka. SSR kontekste XSS gali būti dar pavojingesnis, nes kodas gali būti įterptas į pradinį HTML response.

Next.js pavyzdys su saugiu duomenų perdavimu:

// pages/post/[id].js
export async function getServerSideProps({ params }) {
  const post = await getPost(params.id);
  
  // Valome HTML serverio pusėje
  const cleanContent = DOMPurify.sanitize(post.content);
  
  return {
    props: {
      post: {
        ...post,
        content: cleanContent
      }
    }
  };
}

export default function Post({ post }) {
  // Dabar saugu naudoti dangerouslySetInnerHTML
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Svarbu paminėti ir hydration procesą – kai serverio sugeneruotas HTML tampa interaktyviu kliento pusėje. Šiame etape reikia užtikrinti, kad duomenys būtų validuojami abiejose pusėse.

Kas toliau: automatizuotas testavimas ir monitoring

Geriausia apsauga – tai preventyvi apsauga. Vien tik žinoti apie XSS nepakanka, reikia sistemingai tikrinti kodą ir stebėti aplikaciją production’e.

Automatizuotas testavimas turėtų būti jūsų CI/CD pipeline dalis. Yra keletas įrankių, kurie gali padėti:

**ESLint su security plugin’ais** – statinė kodo analizė gali pagauti daugelį akivaizdžių klaidų:

// .eslintrc.js
module.exports = {
  plugins: ['security', 'react-security'],
  extends: [
    'plugin:security/recommended',
    'plugin:react-security/recommended'
  ],
  rules: {
    'react/no-danger': 'error',
    'security/detect-object-injection': 'warn'
  }
};

**SAST (Static Application Security Testing) įrankiai** kaip SonarQube, Snyk ar Semgrep gali atlikti gilesnę analizę ir rasti sudėtingesnius saugumo trūkumus.

**DAST (Dynamic Application Security Testing)** – įrankiai kaip OWASP ZAP ar Burp Suite gali testuoti veikiančią aplikaciją ir bandyti įterpti XSS payload’us:

// Galite integruoti į CI/CD
npm install -g zaproxy
zap-cli quick-scan --self-contained --start-options '-config api.disablekey=true' \
  https://your-app.com

Production’e svarbu turėti monitoring’ą, kuris aptiktų įtartinus veiksmus. Jei staiga pradeda vykti daug JavaScript klaidų arba matote neįprastus request’us, tai gali būti XSS atakos požymis.

CSP violation report’ai yra puikus būdas stebėti potencialias atakas:

Content-Security-Policy: default-src 'self'; report-uri /csp-violation-report;

Kai naršyklė aptinka CSP pažeidimą, ji siunčia report’ą į jūsų endpoint’ą. Galite analizuoti šiuos duomenis ir reaguoti į realias atakas.

Dar vienas svarbus aspektas – reguliarūs security audit’ai. Bent kartą per metus (o geriau dažniau) turėtumėte atlikti išsamų saugumo patikrinimą. Galite samdyti išorinius specialistus arba naudoti bug bounty platformas kaip HackerOne ar Bugcrowd.

Realūs atvejai ir pamokos iš klaidų

Teorija teorija, bet pažiūrėkime į realius atvejus. 2023 metais viena populiari e-commerce platforma patyrė XSS ataką per produktų atsiliepimų sistemą. Problema buvo ta, kad jie naudojo Vue.js su v-html direktyva, manydami, kad duomenys iš duomenų bazės yra saugūs.

Piktavalis sukūrė produkto atsiliepimą su įterptais script tag’ais. Kai kiti vartotojai peržiūrėjo tą produktą, script’as vykdėsi ir siuntė jų session cookie į piktavalio serverį. Per kelias valandas buvo pažeista šimtai paskyrų.

Sprendimas buvo paprastas – įdiegti DOMPurify ir išvalyti visus atsiliepimus prieš juos atvaizduojant. Bet žala jau buvo padaryta, o kompanijos reputacija nukentėjo.

Kitas įdomus atvejis – startup’as, kuris kūrė social media dashboard’ą su React’u. Jie leido vartotojams įterpti custom CSS stilius savo profiliams. Problema – CSS taip pat gali būti pavojingas:

/* XSS per CSS */
body {
  background: url('javascript:alert("XSS")');
}

/* Arba per expression (seni IE) */
div {
  width: expression(alert('XSS'));
}

Nors modernios naršyklės blokuoja daugelį šių technikų, vis dar yra būdų išnaudoti CSS. Sprendimas – griežtai validuoti CSS ir naudoti CSP su style-src direktyva.

Pamoka iš šių atvejų – niekada nemanykite, kad esate saugūs. XSS atakos evoliucionuoja, ir tai, kas šiandien veikia, rytoj gali būti pažeidžiama. Nuolatinis mokymasis ir atnaujinimai yra būtini.

Dar vienas svarbus dalykas – komandinis darbas. Saugumas nėra tik vieno programuotojo atsakomybė. Visi komandos nariai turėtų suprasti XSS grėsmes ir žinoti, kaip jų išvengti. Code review procese turėtų būti tikrinama ne tik funkcionalumas, bet ir saugumas.

Rekomenduoju reguliariai organizuoti security training’us komandai. Galite naudoti platformas kaip OWASP WebGoat ar HackTheBox, kur galima praktiškai išmokti apie įvairias atakas ir gynybos mechanizmus.

Ateities perspektyvos ir naujos technologijos

Žvelgiant į ateitį, matome kelis įdomius pokyčius. Framework’ai tampa vis saugesni pagal nutylėjimą. Pavyzdžiui, naujos React versijos eksperimentuoja su automatine sanitization net ir dangerouslySetInnerHTML atveju.

Web Assembly (WASM) atneša naujų galimybių, bet ir naujų iššūkių. WASM moduliai gali vykdyti kodą labai greitai, bet jei jie apdoroja nepatikrintus duomenis, gali atsirasti naujų saugumo spragų.

Trusted Types API – tai nauja naršyklės funkcija, kuri padeda kovoti su DOM-based XSS. Ji verčia programuotojus eksplicitiškai nurodyti, kada duomenys yra saugūs:

// Trusted Types pavyzdys
const policy = trustedTypes.createPolicy('myPolicy', {
  createHTML: (string) => {
    // Čia galite valyti HTML
    return DOMPurify.sanitize(string);
  }
});

element.innerHTML = policy.createHTML(userInput);

Kai Trusted Types yra įjungti per CSP, bet koks bandymas priskirti string’ą į innerHTML be policy bus užblokuotas. Tai priverčia programuotojus galvoti apie saugumą.

AI ir machine learning taip pat pradeda būti naudojami saugumo srityje. Yra įrankių, kurie gali analizuoti kodą ir automatiškai aptikti potencialias XSS spragas. GitHub Copilot ir panašūs įrankiai mokosi rašyti saugesnį kodą.

Bet technologijos nėra stebuklingas sprendimas. Pagrindas visada bus geros praktikos, atidus kodas ir nuolatinis budėjimas. Framework’ai gali padėti, bet galutinė atsakomybė visada lieka programuotojui.

Svarbu ir tai, kad saugumas turi būti integruotas į visą development procesą – nuo projektavimo iki deployment’o. Security by design principas reiškia, kad apie saugumą galvojame nuo pat pradžių, o ne bandome jį „priklijuoti” vėliau.

Modernūs framework’ai duoda mums puikius įrankius kovai su XSS, bet jie nėra visagaliai. Supratimas, kaip veikia XSS atakos, kur slypi pavojai ir kaip tinkamai naudoti framework’ų apsaugos mechanizmus – tai yra raktas į saugias web aplikacijas. Nepamirškite, kad saugumas – tai ne vienkartinis veiksmas, o nuolatinis procesas. Mokykitės, testuokite, stebėkite ir visada būkite vienu žingsniu priekyje potencialių piktavalių.

Daugiau

Traefik 3 su Docker compose

Oxc: Rust JavaScript toolchain