Lyhyesti: XSS estetään PHP:ssä koodaamalla jokainen tuloste sen yhteyden mukaan: htmlspecialchars() HTML:lle, json_encode() suojavalitsimin JavaScriptille, tarkistetut URL-osoitteet linkeille. Automaattisesti escapoivat mallit hoitavat tästä suuren osan. Content Security Policy ja HttpOnly-evästeet rajoittavat vahinkoa, jos aukko kuitenkin jää.
Miksi Cross-Site Scripting kannattaa tarkistaa nyt
Lokakuussa 2026 huomio on PHP2Shellissä, kahdessa kriittisessä haavoittuvuudessa PHP-tulkissa, joihin on ollut korjaukset 21.10.2026 alkaen. Moni yritys tarkistuttaa verkkosovelluksensa tässä yhteydessä. Se on hyvä tilaisuus katsoa myös oman koodin heikkouksia, joita mikään PHP-päivitys ei korjaa.
Cross-Site Scripting eli XSS on yksi niistä. OWASP Top 10 käsittelee XSS:ää Injection-luokassa. Toisin kuin palvelimen aukossa, hyökkäys kohdistuu tässä verkkosivustonne kävijöihin ja käyttäjiin, esimerkiksi verkkokaupan asiakkaisiin tai hallintapaneelin työntekijöihin.
Seuraukset voivat olla merkittäviä: ylläpitäjien kaapatut istunnot, manipuloidut lomakkeet, jotka lähettävät tietoja kolmansille, tai väärennetty sisältö verkkosivustollanne. Jos ylläpitäjän tili kaapataan, seurauksena voi olla sovelluksen täydellinen hallinta.
Miten XSS syntyy PHP-sovelluksissa?
XSS syntyy, kun sovellus tulostaa sivulle tarkistamatta tietoja epäluotettavasta lähteestä. Selain ei enää erota sisältöänne lisätystä koodista ja suorittaa molemmat.
Muotoja on kolme. Heijastetussa XSS:ssä sisältö tulee suoraan nykyisestä pyynnöstä, esimerkiksi URL-osoitteen hakusanasta. Tallennetussa XSS:ssä se on tietokannassa, esimerkiksi kommentissa tai profiilikentässä, ja osuu jokaiseen, joka avaa sivun. DOM-pohjainen XSS syntyy selaimen JavaScriptissä, kun skriptit kirjoittavat URL-osoitteen arvoja sivulle.
Epäluotettavia ovat kaikki tiedot, jotka eivät ole kiinteästi koodissa: lomakekentät, URL-parametrit, evästeet, tiedostonimet, ulkoisten rajapintojen vastaukset ja oman tietokannan sisältö, joka on jossain vaiheessa tullut ulkopuolelta.
Tulosteen oikea koodaus yhteyden mukaan
Tärkein sääntö kuuluu: koodatkaa tulostettaessa, ja sen paikan mukaan, johon arvo päätyy. HTML-sisällölle ja lainausmerkeissä oleville attribuuteille htmlspecialchars() on oikea väline. Pieni apufunktio estää asetusten unohtumisen:
<?php
function e(?string $wert): string
{
return htmlspecialchars($wert ?? '', ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
?>
<h2><?= e($produkt['name']) ?></h2>
<input type="text" name="suche" value="<?= e($_GET['q'] ?? '') ?>">
Muut yhteydet vaativat muita keinoja:
| Yhteys | Turvallinen menetelmä |
|---|---|
| HTML-sisältö, lainausmerkeissä oleva attribuutti | htmlspecialchars() asetuksilla ENT_QUOTES ja UTF-8 |
| JavaScriptin tiedot | json_encode() asetuksilla JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_HEX_QUOT |
| URL-osoitteen parametri | rawurlencode() yksittäiselle arvolle |
| Täydellinen URL-osoite käyttäjän syötteestä | Tarkista skeema, salli vain https ja http, sen jälkeen htmlspecialchars() |
| Attribuutit ilman lainausmerkkejä, tapahtumankäsittelijät, CSS | vältä, älä sijoita arvoja niihin |
Skriptin tarvitsemille tiedoille data-attribuutti tai JSON-lohko on yleensä siistein tapa:
<?php
$daten = json_encode($konfiguration, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<div id="app" data-konfig="<?= e($daten) ?>"></div>
JavaScriptissä luette arvon sitten data-attribuutin kautta ja asetatte sisällön textContent-ominaisuudella innerHTML-ominaisuuden sijaan.
Mallit, HTML:n puhdistus ja viitekehykset
Mallimoottorit kuten Twig ja Blade koodaavat tulosteet oletuksena automaattisesti. Se sulkee pois suuren osan tyypillisistä virheistä. Vaaralliseksi tilanne muuttuu siellä, missä tämä käytös kytketään tietoisesti pois, esimerkiksi Twigin raw-suodattimella tai Bladen syntaksilla {!! !!}. Tällaisia kohtia pitäisi olla koodissa vähän, ja ne tulisi tarkistaa kohdennetusti jokaisessa katselmoinnissa.
Jos sovelluksenne sallii tietoisesti käyttäjien HTML:n, esimerkiksi muotoilut kommenteissa tai sisällönhallintajärjestelmässä, koodaus ei riitä, koska HTML:n on tarkoitus näkyä. Silloin tarvitsette tarkistetun puhdistuskirjaston kuten HTML Purifier, joka päästää läpi vain kiinteän luettelon sallittuja elementtejä ja attribuutteja. Säännöllisillä lausekkeilla tehdyt omat suodattimet ovat kokemuksen mukaan aukkoisia.
Tarkistakaa erityisesti hallintapuolen alueet. XSS näkymissä, jotka näkevät vain ylläpitäjät, aliarvioidaan usein, mutta se osuu juuri tileihin, joilla on eniten oikeuksia.
XSS:n löytäminen vanhasta PHP-koodista
Vanhoissa sovelluksissa ensimmäinen kysymys ei yleensä ole, miten koodataan oikein, vaan missä se on toistaiseksi unohtunut. Käytännöllinen aloitus on haku kohtiin, joissa muuttujia tulostetaan suoraan:
# Pyyntötietojen suorat tulosteet
grep -rnE "(echo|print|<\?=)\s*\\\$_(GET|POST|REQUEST|COOKIE|SERVER)" src/ templates/ --include="*.php"
# Twigin ja Bladen automaattinen escapointi poistettu käytöstä
grep -rnE "\|raw|\{!!" templates/ resources/views/
Osumat ovat tarkistustehtäviä, eivät valmiita löydöksiä. Tallennetun XSS:n tällainen haku löytää vain osittain, koska arvot tulevat siellä tietokannasta. Tarkistakaa siksi lisäksi kaikki näkymät, jotka näyttävät käyttäjien sisältöä: kommentit, profiilit, tilausmuistiinpanot, tukitiketit. CMS-järjestelmissä ja verkkokaupoissa kolmansien osapuolten lisäosat ja teemat kuuluvat nimenomaisesti mukaan, sillä niissä on suuri osa tulostelogiikasta.
Toinen puolustuslinja: CSP ja turvalliset evästeet
Huolellisessakin koodissa yksi kohta voi jäädä huomaamatta. Kaksi toimenpidettä rajoittaa silloin vahinkoa. Content Security Policy sallii selaimelle vain skriptit määritellyistä lähteistä ja kieltää upotetut skriptit, ellei niitä ole nimenomaisesti sallittu:
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Ottakaa tällainen linjaus käyttöön vaiheittain, aluksi pelkässä raportointitilassa otsakkeella Content-Security-Policy-Report-Only, sillä analytiikka- tai maksupalveluiden nykyiset upotukset on yleensä lisättävä. Muita otsakkeita kuvaa opas Verkkosivustojen tietoturvaotsakkeet.
Lisäksi istuntoevästeillä pitäisi olla HttpOnly-määre, jotta skriptit eivät voi lukea niitä. Miten suojaatte istunnot kokonaisuudessaan, kerrotaan oppaassa Istuntojen suojaaminen PHP:ssä. Aiheeseen liittyvää heikkoutta tietokantapuolella käsittelee SQL-injektion estäminen PHP:ssä.
Miten PHP SOS auttaa konkreettisesti?
- Tarkistamme sovelluksenne mallit ja tulosteet puuttuvan tai väärän koodauksen varalta.
- Arvioimme löydökset tavoitettavuuden ja koskettujen tilien mukaan ja näytämme konkreettiset korjaukset.
- Otamme Content Security Policyn käyttöön vaiheittain häiritsemättä verkkokaupan tai maksujen toimintoja.
- Testaamme korjauksen jälkeen uudelleen ja dokumentoimme tuloksen.
Riippumattoman tarkistuksen kuvaa palvelu Tietoturva-auditointi ja penetraatiotesti.
Usein kysytyt kysymykset
Riittääkö htmlspecialchars XSS:ää vastaan?
Tavalliselle tekstille HTML-sisällössä ja lainausmerkeissä olevissa attribuuteissa kyllä, kun annatte ENT_QUOTES-asetuksen ja merkistön UTF-8. JavaScriptissä, URL-osoitteissa tai CSS:ssä pätevät muut säännöt, ja siellä tarvitaan oma, sopiva koodaus.
Pitäisikö syötteet suodattaa tallennettaessa vai tulostettaessa?
Koodatkaa tulostettaessa, kohteeseen sopivalla tavalla. Syötteiden uskottavuus kannattaa tarkistaa vastaanotettaessa, esimerkiksi pituus ja muoto. Joka suodattaa vain tallennettaessa, ohittaa muista lähteistä tulevat tiedot ja vaikeuttaa myöhempää käyttöä muissa yhteyksissä.
Mitä hyötyä Content Security Policystä on?
Content Security Policy määrittää, mistä lähteistä selain saa ladata ja suorittaa skriptejä. Se voi rajoittaa huomaamatta jääneen XSS-aukon vaikutuksia selvästi, mutta ei korvaa oikeaa koodausta.
Miten sallin muotoilut, kuten lihavoinnin tai linkit, kommenteissa?
Käyttäkää tarkistettua HTML-puhdistuskirjastoa, jossa on sallittujen elementtien ja attribuuttien luettelo. Omat säännölliset lausekkeet skriptien poistamiseen ovat virhealttiita, eikä niitä pitäisi käyttää.