Globaliserade webbapplikationer, dvs med stöd för världens alla språk, är bra ur användbarhetssynpunkt. Men det har lett till en rad säkerhetsproblem, primärt spoofing och kanonikaliseringsproblem. Ämnet behandlas i "Unicode Technical Report #36 UNICODE SECURITY CONSIDERATIONS av Mark Davis and Michel Suignard och dess refererade rfc:er. Jag serverar er en sammanfattning (med några personliga tillägg).
Tecken visuellt lika varann
Pixelgrafiken har länge gjort att vissa tecken liknar varann, särskilt i låga upplösningar. Till exempel I och l eller O och 0. Också kombinationer av tecken kan lura ögat, exempelvis 'rn' som i vissa typsnitt ser ut som 'm'. Med unicodes över 96.000 tecken så har antalet visuella kollisioner ökat dramatiskt.
Spoofade domännamn
När domännamn tilläts innehålla unicode-tecken och inte bara ASCII (dvs införandet av Internationalized Domain Names, IDN) så öppnades en ny säkerhetsrisk -- spoofade domännamn. Ta till exempel www.projektplаtsen.se och www.projektplatsen.se. Det är inte samma adress! Ser ni skillnaden? OK, jag ger er unicode som hjälp:
www.projektplаtsen.se
0077 0077 0077 002e 0070 0072 006f 006a
0065 006b 0074 0070 006c 0430 0074 0073
0065 006e 002e 0073 0065
www.projektplatsen.se
0077 0077 0077 002e 0070 0072 006f 006a
0065 006b 0074 0070 006c 0061 0074 0073
0065 006e 002e 0073 0065
Det är alltså ett kyrilliskt 'a' som spökar.
De vanligast förekommande spoof-tecknena är de kyrilliska bokstäverna 'асһеіјорѕху' som i många typsnitt är skrämmande lika de latinska 'acheijopsxy'. Teckenkoderna är förstås helt olika:
асһеіјорѕху (kyrilliska)
0430 0441 04bb 0435 0456 0458 043e 0440
0455 0445 0443
acheijopsxy (latin)
0061 0063 0068 0065 0069 006a 006f 0070
0073 0078 0079
Ett specialfall med bärighet på oss i Sverige är 'ä'. I unicode kan det både skrivas som den unika bokstaven 'ä' (unicode 00e4) och som 'a¨' (unicode 0061 0308). Men domännamn måste vara unika efter en så kallad kompatiblitetsnormalisering till ASCII Compatible Encoding, ACE. Det gör att båda varianterna av 'ä' kommer ge samma unika domännamn.
Toppdomänen (.se, .com osv) är fortfarande bara ASCII så där kan inte spoofning ske. Förutsatt att ingen spoofar punkten förstås. Tecknet '·' visas i en del typsnitt som en vanlig punkt.
En .se-domän får enligt IIS innehålla bokstäverna a-z, å, ä, ö, é och ü samt siffrorna 0-9, bindestreck och skrivtecken som förekommer i de officiella svenska minoritetsspråken eller i våra nordiska grannländers språk.
För dem av er som verkligen vill veta vilka tecken som tillåts i IDN så finns unicode.org:s kompletta lista.
Mixed-Script Spoofing
Exemplet med projektplatsen.se hade bokstäver från två alfabet. Spoofning via blandning av olika alfabet kallas mixed-script spoofing. Frågan är om vi borde förbjuda domäner eller text med mixade alfabet? Ska vi t ex förbjuda domännamn med 13 latinska bokstäver plus en kyrillisk som i fallet med projektplatsen.se? Nja. Länder med icke-latinska tecken lever i samma anglifierade värld som vi och vill mixa sina egna bokstäver med latinska, t ex en rysk domän för XML-dokument: XML-документы.com.
Men det kan vara intressant att detektera mixade alfabet i sin indatavalidering. Exempelkod i Java från unicode.org:
/**
* Returns the script of the input text. Script values of COMMON and INHERITED are ignored.
* @param source Input text.
* @return Script value found in the text.
* If more than one script values are found, then UScript.INVALID_CODE is returned.
* If no script value is found (other than COMMON or INHERITED), then UScript.COMMON is returned.
*/
public static int getSingleScript(String source) {
if (source.length() == 0) {
return UScript.COMMON;
}
int lastScript = UScript.COMMON; // temporary value
int cp;
for (int i = 0; i < source.length(); i += UTF16.getCharCount(cp)) {
cp = UTF16.charAt(source, i);
int script = UScript.getScript(cp);
if (script == UScript.COMMON || script == UScript.INHERITED) {
continue;
}
if (lastScript == UScript.COMMON) {
lastScript = script;
} else if (script != lastScript) {
return UScript.INVALID_CODE;
}
}
return lastScript;
}
Single-Script Spoofing
Att spoofa med hjälp av tecken från ett enda språk/alfabet kallas single-script spoofing. Och det finns många möjligheter att spoofa inom ett språk. Ta till exempel skillnanden mellan ett minustecken '-' och ett riktigt bindestreck '‐':
a-b.com
0061 002d 0062 002e 0063 006f 006d
a‐b.com
0061 2010 0062 002e 0063 006f 006d
Det skulle inte mixed-script detection-koden upptäcka. I Sydostasien finns det till och med exempel på tecken som kan kombineras i olika ordning men ger samma visuella text. För er med alla teckentypsnitt installerade så kan ni prova 101c 102d 102f och 101c 102f 102d.
Domännamn från höger till vänster?
Exempelvis arabiska och hebreiska skrivs från höger till vänster, och de tillåts i internationella domännamn. För att hantera texter med en mix av vänster-till-höger och höger-till-vänster (t ex siffror som alltid skrivs vänster till höger) så finns Unicode Bidirectional Algorithm med en specifikation som får quick sort att se ut som Hello World.
Vissa tecken är dessutom riktningsneutrala, till exempel skiljetecken som '.' och '?'. Deras riktning beror på omgivande teckens riktning. Som exempel har vi nedan en domän där ordningen på två arabiska labels i en path byts om en latinsk label kommer med i mitten och "ändrar riktning" på de separerande punkterna:
http://سلام.دائم.com
http://سلام.a.دائم.com
Det här är som upplagt för bidirectional spoofing där man tror att det är en vanlig vänster-till-höger-domän men den egentligen ska läsas höger till vänster. Därför har man man specat följande skall-krav (2. Preparation Overview -> 4 Check bidi):
- Varje distinkt label i ett domännamn (label3.label2.label1.com) skall bestå av tecken som skrivs vänster-till-höger eller höger-till-vänster, inte både och.
- Om en label har tecken som skrivs höger till vänster så skall första och sista tecknet vara strikt höger-till-vänster, ej riktningsneutralt.
Snedstreck och sneda streck
Har ni kollat in unicode-tecknet 2044? Så här ser det ut '⁄ '. Jaha, kan det vara farligt? Ja, om vi tänker oss följande webbadress ...
www.dinbank.se⁄ login⁄ checkUser.jsp?inxs.ch
... så är det inte många som skulle förstå att den tillhör domänen inxs.ch. Den typen av attack sorterar under syntax spoofing och utnyttjar att unicode har tecken som liknar vanliga escape-tecken.
Några andra läskiga tecken
Det finns mer läskigheter. Vad sägs om följande unicode-tecken:
- 20A8: '₨' som är skrämmande likt 'Rs'
- 200D: '', zero width joiner som inte har något synligt tecken alls (prova att kopiera till en texteditor och "stega" igenom med piltangenterna så märker du att det faktiskt finns ett tecken där emellan fnuttarna)
- 0000 (dvs null) som funkar utmärkt i Javasträngar men inte syns. Testa koden nedan:
public class NullInStringTester {
public static void main(String[] args) {
String strWithNull = "John" + '\u0000' + "Wilander";
System.out.println("Before replacement: " + strWithNull);
String strNullReplaced = strWithNull.replace('\u0000', 'X');
System.out.println("After replacement: " + strNullReplaced);
}
}
Välj nivå på valideringen
Så hur ska man hantera det här? Ja, Mark och Michel föreslår att man först väljer en av fem nivåer i fallande strikthet:
- ASCII-Only: Alla tecken måste vara ASCII
- Highly Restrictive: Alla tecken i varje identifierare eller token ska komma från ett alfabet/språk eller från någon av kombinationerna ASCII + Han + Hiragana + Katakana; ASCII + Han + Bopomofo; eller ASCII + Han + Hangul
- Moderately Restrictive: Tillåt latinska tecken ihop med andra alfabet/språk förutom kyrilliska, grekiska och Cherokee. I övrigt som Highly Restrictive.
- Minimally Restrictive: Tillåt valfri blandning av alfabet/språk som exempelvis Ωmega, Teχ, HλLF-LIFE och Toys-Я-Us. I övrigt som Moderately Restrictive
- Unrestricted: Helt fritt, exempelvis I♥NY.org
Generella tips till programmeraren
Tips till er som skriver kod och ska hantera unicode, till exempel UTF-8:
- Om du parse:ar nummer och tal -- upptäck siffror från oväntade språk eller i blandning mellan olika språk och varna användaren.
- Om du definierar format på identifierarere i programmeringsspråk, protokoll och liknande -- använd "Security Profile for General Identifiers".
- Vid ekvivalenstest av identifierare -- kör transformen Normalization Form KC (NFKC) Compatibility Decomposition followed by Canonical Composition samt toLowerCase() för att nå kanonisk form (exempelkod i Java). Visa användaren de kanonikaliserade identifierarna.
- Om du konfigurerar typsnitt -- använd en storlek som gör att teckenskillnader syns och se till att omöjliggöra alltför små tecken.
- Om du stöter på okända eller osupportade tecken -- visa aldrig ett vanligt frågetecken eller inget tecken alls, utan visa teckenkoden i hex eller en svart fyrkant med ett frågetecken i (unicode fffd).
Specifika UTF-8-attacker
UTF-8 är ett av tre format för unicode (de andra är UTF-16 och UTF-32). UTF-8 är av variabel längd så ett tecken kan vara mellan en och fyra bytes.
Innan version 3.0 av unicode så var det tillåtet att tolka UTF-8 utan att den var på kortast möjliga form. Det gav upphov till att UTF-8 och UTF-16 kunde tolkas olika. Numer är det bara kortast möjliga form (shortest form) som gäller.
Om man konsumerar en UTF-8-ström byte för byte och stöter på en otillåten byte så får man inte hoppa över den ihop med kommande byte(s). Det har nämligen använts för att få filter att göra fel. Om vi tänker oss följande html:
<span style="width:100%" ?> onMouseOver=doBadStuff() ...
... där ? representerar den otillåtna byte:en c2. Om vi då kastar c2 ihop med nästkommande byte i tron att det är ett tvåbytes-tecken så kastar vi '>' och onMouseOver hamnar innanför span-taggen.
Dags att sätta punkt
Det är dags att sätta punkt för det här långa blogginlägget. Frågan är bara om jag ska sätta ...
- 002e (full stop)
- 3002 (ideographic full stop)
- ff0e (fullwidth full stop) eller
- ff61 (halfwidth ideographic full stop)
... som alla ska tolkas som punkt enligt RFC 3490, Internationalizing Domain Names in Applications :).