UPDATE - since this original post, we added new data manipulation capabilities to v2.6.0 with the introduction of the @rsub operator. See the last section on modifying outbound data server-side.
This week's installment of Detecting Malice with ModSecurity will discuss how to detect and prevent Cross-Site Request Forgery (CSRF) Attacks.
Example CSRF Section of Robert "Rsnake" Hansen's book "Detecting Malice" -
One form of attack that is widely found to be present on most websites is cross site request forgery (CSRF). Basically, an attacker can force a victim's browser to connect to your site, and perform functions, like change password, change email address, and so on, unless you have some protection in place to defend yourself. The reason why CSRF is so difficult is because the Internet was designed to allow a user to connect to anything they wanted to from anywhere they wanted to, given there was a routable address. Unfortunately that leads to the possibility of attack. Here's the process flow:
1. Attacker visits a website "site1.com" that he wants to steal user accounts from.
2. Attacker realizes that the website's change password function is vulnerable to CSRF which will allow him to reset people's passwords.
3. Attacker also knows that the administrator of site1.com visits a web-board on "site2.com" which will allow the attacker to insert references to images.
4. Attacker inserts an image tag into site2.com's web-board similar to <img src="http://site1.com/passwordchange?pass=1234&pass2=1234">
5. The administrator of site1.com visits the site2.com's web-board and his browser is immediately redirected back to his own website where he is authenticated.
6. The attacker logs into site1.com with the administrator's username and the password "1234" successfully.
This is only one of dozens of ways to perform a CSRF attack, and to be clear, although the name specifically says "cross" site, it doesn't necessarily have to involve two websites for this same attack to work, if the original site also had a web-board, for instance, that is vulnerable to HTML injection.
So how can we mitigate CSRF attacks? Detecting Malice suggests the following:
Note: Although it is possible to catch CSRF attacks by observing the logs carefully, as demonstrated above, your site should not allow such attacks to work in the first place. If they do, that only means the site wasn't coded properly, and you should generally seek to determine if it is possible to fix the problem in the code. The preferable method of prevention is to use one time tokens or "nonces" that are unique to every page view, and not known to an attacker. These nonces are sent only once, and verified by the server to ensure that the user could have been the only one who had access to that page. Of course if the page suffers from other attacks that enable an attacker to read the nonce, like XSS, that's another issue.
OWASP has a great project called CSRFGuard which implements a framework for using request validation tokens. I am using the an older version of the JS file in these examples. The main limitation is that the older csrf JS injection code does not update AJAX calls. I haven't had a chance yet to review the new v3 release yet.
The OWASP ModSecurity CRS Project includes an optional rules file that will implement request validation tokens. The file is called modsecurity_crs_43_csrf_protection.conf. Before injecting or validating CSRF tokens, you first need to activate another file called - modsecurity_crs_16_session_hijacking.conf. This is a helper file whose main purpose is to create a Session-based persistent collection using the application's Set-Cookie SessionID data as a key. It will then store the csrf token value in the collection for use in the Content Injection action in the csrf protection file.
## This rule will identify the outbound Set-Cookie SessionID data and capture it in a setsid#SecRule RESPONSE_HEADERS:/Set-Cookie2?/ "(?i:(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid)=([^\s]+)\;\s?)" "chain,phase:3,t:none,pass,nolog,capture,setsid:%{TX.6},setvar:session.sessionid=%{TX.6},setvar:tx.ip=%{remote_addr},setvar:tx.ua=%{request_headers.user-agent},setvar:session.valid=1" SecRule SESSION:SESSIONID "(.*)" "t:none,t:sha1,t:hexEncode,capture,setvar:session.csrf_token=%{TX.1}"
As a demonstration, I setup my local ModSecurity system to be a forward proxy so that I could browse through it to any website. I decided to test out the CSRF Protections against Amazon's site. When I made an initial request for the ModSecurity Handbook page, this transactional data was logged in the ModSecurity audit log file:
--b48e4530-A--[11/Jan/2011:14:22:18 --0500] TSyt6MCoAWoAAIqiGLMAAAAA 127.0.0.1 54004 127.0.0.1 80--b48e4530-B--GET http://www.amazon.com/ModSecurity-Handbook-Ivan-Ristic/dp/1907117024/ref=sr_1_1?s=books&ie=UTF8&qid=1294773447&sr=1-1 HTTP/1.1Host: www.amazon.comUser-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7Keep-Alive: 115Proxy-Connection: keep-alive
--b48e4530-F--HTTP/1.1 200 OKx-amz-id-1: 0EAZTWRYA937PQMP6HRDp3p: policyref="http://www.amazon.com/w3c/p3p.xml",CP="CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC "x-amz-id-2: 7rwkjLgOBJWUFf4XFksZpyuFePZTQUz9QRD6PsgBkVHAqBCrtC8m4KXCV1JuL9CuVary: Accept-Encoding,User-AgentCneonction: closeContent-Type: text/html; charset=ISO-8859-1Set-cookie: session-id=190-2550070-1804850; path=/; domain=.amazon.com; expires=Tue Jan 01 08:00:01 2036 GMT; HTTPOnlyContent-Length: 381900
You can see the Amazon is issuing a Set-Cookie to hold my SessionID in the session-id cookie name. Our previous rule will now capture this session-id value and use it as a key for the Session collection. We also capture important meta-data including:
This is how it looks in the debug log when we save this data to the Session collection:
Wrote variable: name "__expire_KEY", value "1294778739".Wrote variable: name "KEY", value "190-2550070-1804850".Wrote variable: name "TIMEOUT", value "3600".Wrote variable: name "__key", value "190-2550070-1804850".Wrote variable: name "__name", value "default_SESSION".Wrote variable: name "CREATE_TIME", value "1294775137".Wrote variable: name "UPDATE_COUNTER", value "1".Wrote variable: name "sessionid", value "190-2550070-1804850".Wrote variable: name "valid", value "1".Wrote variable: name "csrf_token", value "ab411ea78241aae1074a2f7587e270f5648a9fa1".Wrote variable: name "ip", value "a90c80bf0f0fc7f79224cd129783eaa26c358d5b".Wrote variable: name "ua", value "f702ef57b6b7ca1e9382e0b2c2e1399ba54793f9".Wrote variable: name "LAST_UPDATE_TIME", value "1294775139".Persisted collection (name "default_SESSION", key "190-2550070-1804850").
Now that we have a csrf token for each session, we can then use the following csrf rules to inject the CSRFGuard JS data into the response:
## This rule will use Content Injection to append the CSRF Token #SecRule &SESSION:CSRF_TOKEN "@eq 1" \"phase:4,t:none,nolog,pass,append:'<html><script language=\"JavaScript\"> \\var tokenName = \'CSRF_TOKEN\'; \var tokenValue = \'%{session.csrf_token}\'; \\function updateTags() { \\ var all = document.all ? document.all : document.getElementsByTagName(\'*\'); \ var len = all.length; \\ for(var i=0; i<len; i++) { \ var e = all[i]; \ \ updateTag(e, \'src\');\ updateTag(e, \'href\');\ } \} \\--CUT--\updateTags(); \updateForms(); \\</script></html>'"
Notice that the ModSecurity append action is being used to inject the anti-csrf token into the response body. The append action uses macro expansion so that the "%{session.csrf_token}" data is properly expanded to the hash value saved in the collection. When injected into the response body, the resulting HTML ending looks like this:
This JS code will update all links and forms and add in the new CSRF_TOKEN value. Here is another screenshot of an updated link. Notice as I mouse over the link that the URL location at the bottom of the browser window shows the link with the updated CSRF_TOKEN value.
When the user then clicks on links or submits forms, etc... the following ModSecurity CSRF rules can validate that the token is present and that it has the correct value for the SessionID.
SecRule &ARGS "@ge 1" "chain,phase:2,t:none,block,msg:'CSRF Attack Detected - Missing CSRF Token.'" SecRule &ARGS:CSRF_TOKEN "!@eq 1" "setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{critical_anomaly_score},setvar:tx.%{rule.id}-WEB_ATTACK/CSRF-%{matched_var_name}=%{matched_var}"
SecRule &ARGS "@ge 1" "chain,phase:2,t:none,block,msg:'CSRF Attack Detected - Invalid Token.'" SecRule ARGS:CSRF_TOKEN "!@streq %{SESSION.CSRF_TOKEN}" "setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{critical_anomaly_score},setvar:tx.%{rule.id}-WEB_ATTACK/CSRF-%{matched_var_name}=%{matched_var}"
While all of this works, what if you would rather implement the request validationt token server-side?
With ModSecurity v2.6, we introduced the @rsub operator which provides the capability to modify the live data stream. With this operator, we can now inspect the html response bodies and modify any form fields to insert our CSRF request validation token server-side before sending it to the client.
Here is an example rule:
SecRule STREAM_OUTPUT_BODY "@rsub s/<\/form>/<input type=\"hidden\" name=\"rv_token\" value=\"%{unique_id}\"><\/form>/" \"phase:4,t:none,nolog,pass"
The methodology is to use the new @rsub operator to trap the closing html </form> tag and to then insert our CSRF rv_token right before it. Instead of trying to create a new random hash, the example here uses macro expansion to dynamically use the Apache mod_unique_id token as the CSRF nonce. This is nice as it is automatically generated by Apache so we don't have to create our own hash like we did previosly with the application SessionID data.
We then save this nonce in the ModSecurity SESSION collection (similar to what was described previously) and use it on subsequent requests to validate the rv_token value. This all works well and being done server-side is good as it doesn't rely upon client-side JS. The only limitation is that this will only protect forms. We will be conducting more research to see about modifying query string data and AJAX calls.