fredag 19 mars 2010

JSONP -- medveten XSS med risk för CSRF

Same origin policy (SOP) är vid sidan av webbläsarbråket den ständiga utmaningen i modern webbutveckling. Säkerhetsfolket försvarar den med näbbar och klor, svarthattarna letar säkerhetshål i den, och utvecklarna försöker kringå den för att kunna bygga nya tjänster med innehåll från olika källor. SOP är en strikt policy som antingen är på eller av, konfiguration är inte möjlig.

Same origin policy i ett nötskal
I princip säger SOP att webbinnehåll från olika källor inte får läsa eller ändra på varann. I Unix-termer så säger SOP att e(X)ecute är tillåtet men inte (R)ead eller (W)rite. Några exempel är på sin plats.

Antag att du bygger en webbapplikation med innehåll som du hämtar från flera källor, t ex en kombination av information och funktioner från owasp.org, owasp.se och google.com.
  • Innehåll från http://www.owasp.org får inte läsa eller ändra innehåll från http://www.owasp.se eller http://www.google.com pga olika domäner
  • Innehåll från http://www.owasp.org får inte läsa eller ändra innehåll från https://www.owasp.org pga olika protokoll
  • Innehåll från http://www.owasp.org:80 får inte läsa eller ändra innehåll från http://www.owasp.org:8080 pga olika portar (gäller alla webbläsare utom Internet Explorer)
Kringå SOP med hjälp av JSONP
Det finns flera sätt för utvecklare att arbeta sig runt SOP, en bra summering är Solving Cross-Domain Issues When Building Mashups. En populär teknik heter JSON with Padding (JSONP) och kan beskrivas som medveten cross site scripting.

Man utnyttjar det faktum att JavaScript får laddas från valfri källa, dvs SOP gäller inte script-taggen.

<script>Här får du hämta från valfri källa och köra som JavaScript</script>

Om nu källan istället för att bara svara med JSON (JavaScript Object Notation) lägger till ("paddar") utgående data med ett funktionsanrop så kommer det anropet köras.

Hämta vanlig JSON och stoppa in den mellan script-taggarna:
<script type="text/javascript" src="http://owasp.se/statistics/jsonp/chapterMemberCount"></script>
... resulterar i det icke-fungerande skriptet ...
<script>{"count":"384"}</script>

Men att hämta JSONP med parametern 'callback' och stoppa in mellan script-taggarna:
<script type="text/javascript" src="http://owasp.se/statistics/jsonp/chapterMemberCount?jsoncallback=callback"></script>
... resulterar i det fungerande skriptet ...
<script>callback({"count":"384"})<script>

Om du tänder och släcker script-taggen, dvs skriver ut den och tar bort den, så kan du till och med "polla" en tjänst (en ordentlig beskrivning av hur du gör i ren JavaScript finns på Stefan Petterssons blogg).

En JSONP-klient
Så här kan en JSONP-klient byggd med JQuery se ut, i det här fallet en klient som hämtar antalet medlemmar i OWASP Sweden:

<!DOCTYPE html>
<html>
<head>
<title>Medlemmar i OWASP Sweden</title>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">

URL_COUNT = 'https://owasp.se/statistics/jsonp/chapterMemberCount?&jsoncallback=?';

$(document).ready(function() {
__$.getJSON(URL_COUNT,
____function(data){
______$('#count').html(data.count);
____}
__);
});

</script>
</head>
<body>
__Totalt antal medlemmar i chaptret: <span id="count"></div>
</body>
</html>

En JSONP-tjänst på servern
På serversidan får man använda något ramverk som stödjer JSONP, t ex JAX-RS Jersey som nedan eller den bundlade JSON-pluginen i Struts 2. Här är serversidans RESTful service för antalet medlemmar:

@DisableXMLHttpRequest
@GET
@Path("/statistics/jsonp/chapterMemberCount")
@Produces( { "application/x-javascript", MediaType.APPLICATION_JSON } )
public JSONWithPadding chapterMemberCountJsonP (
______@Context HttpServletRequest request,
______@QueryParam("jsoncallback") String callback)
{
__return new JSONWithPadding( new
______GenericEntity<Statistic>
________(getStats(request)) { }, callback);
}

Medveten Cross Site Scripting?
Hur var det nu med medveten cross site scripting? Jo, det är ju precis vad JSONP är. Du hämtar data från en annan källa och skriver ut det mellan script-taggar. Med andra ord litar du blint på det skript som kommer tillbaka och det finns inget som hindrar källan att lägga till valfri JavaScript som du gladeligen kommer köra. Men eftersom det liksom ligger i designens natur och syfte kan man inte se det som ett säkerhetshål, snarare en medveten risk. Använd med andra ord bara JSONP när du anser dig kunna lita på källan.

Risk för Cross Site Request Forgery?
Sen var det frågan om cross site request forgery (CSRF). I det scenariot så har attackeraren inkluderat request till JSONP-tjänsten på sin sida och alla offer som surfar in där kommer under ytan utföra requesten med sin session. CSRF-attacker är alltid blinda eftersom svaret från tjänsten kommer till offrets webbläsare och inte till attackeraren. Men med JSONP kan attackeraren "padda" med JavaScript som läcker ut informationen och alltså åstadkomma icke-blind CSRF. Det är ett säkerhetshål om informationen anses känslig! Och det hålet måste i så fall åtgärdas på servern precis som vid vanlig CSRF, dvs med en token som klienten förväntas skicka tillbaka. Det lite otrevliga med det är att din RESTful service inte blir tillståndslös vilket den egentligen ska vara.

@DisableXMLHttpRequest
@GET
@Path("/statistics/jsonp/chapterMemberCount")
@Produces( { "application/x-javascript", MediaType.APPLICATION_JSON } )
public JSONWithPadding chapterMemberCountJsonP (
______@Context HttpServletRequest request,
______@QueryParam("jsoncallback") String callback,
______@QueryParam("token") String antiCsrfToken )
{
__// Check the token
__...
__return new JSONWithPadding(
______new GenericEntity<Statistic>
________(getStats(request)) { }, callback);
}


(Tack Stefan Pettersson, Netlight, för ditt bidrag till det här blogginlägget!)

Inga kommentarer: