Clojure Web Security
Summary: Use the OWASP Top Ten Project to minimize security vulnerabilities in your Clojure web application.
NOTE: This article is from 2013. I have updated the broken links but the important security issues have changed over time—as well as the Clojure resources available to address them. I still need to update this to reflect those.
Aaron Bedra gave a very damning talk about the security of Clojure web applications. He went so far as to say that Clojure web apps are some of the worst he has seen. You should watch the talk. He has some good recommendations.
One of the jobs of web frameworks is to handle security concerns inherent in the web itself. Because most Clojure programmers build their own web stack, they often fail to look at the security implications of their application. They do not protect their site from even the easiest and most common forms of vulnerabilities. These vulnerabilities are problems with the way the web works, not with the particular server technology, yet it has become the server's responsibility to mitigate the vulnerabilities. Luckily, the vulnerabilities are well-studied and there are known fixes.
The Open Web Application Security Project (OWASP) does a very good job of documenting common web vulnerabilities and providing good fixes for them. They have a project called the Top Ten Project which every web developer should refer to regularly and use to improve the security of their app. You should also run through the Application Security Verification Standard checklists to audit your code. But the Top Ten should get you to understand the basics.
Warning: I am not a security expert. You should do your own research. The code I present here is my own interpretation of the OWASP recommendations. It has not been audited by experts. Do your own research!
Also, security is an ongoing concern. If you have any comments, suggestions, or questions, please bring them up!
Here is the Top Ten 2013 with a small breakdown and a Clojure solution, if applicable.
A1. Injection
If a server accepts input from the outside and then parses and
interprets that input as a scripting or query language, it is open to
attack. The most common form is SQL Injection, where an input form is
posted to the server, the value of that form is concatenated into a
string to make a SQL statement, and then the SQL statement is sent to
the database to be executed. What happens if a malicious user types in
"'; DELETE FROM USERS;"
?
My preferred solution to SQL Injection in Clojure is to always use
parameterized SQL statements. clojure.java.jdbc
, supports these
directly. The parameters will be escaped, making injection
impossible.
Another problem is if you want to read in some Clojure data from the
client, and you call clojure.core/read-string
on it. read-string
will execute arbitrary Java constructors. For instance:
#java.io.FileWriter["myfile.txt"]
This will create the file myfile.txt
or overwrite if it already
exists. Also, there is a form (called read-eval form) to execute code at
read-time:
#=(println "Hello, vulnerability!")
Read in that string, and it will print. Any code could be in there.
The solution is to never use clojure.core/read-string
. Use
clojure.edn/read-string
, which is a well-documented format. It does
not run arbitrary constructors. It has no read-eval forms.
Summary: Always use parameterized SQL and use clojure.edn/read-string
instead of clojure.core/read-string
on edn input.
A2 Broken Authentication and Session Management
Authentication
This is a big topic and I can't address it all here. Clojure has the Friend library, which is the closest thing we have to a de facto standard. My suggestion is simply to read the entire Friend README and evaluate whether you should use it. This is serious stuff. Read it.
Session Management
Ring provides a session system which is fairly good. It meets many of the OWASP Application Security Verification Standard requirements. But it does not handle all of them automatically. You still need code audits. For instance, if you are logging requests, OWASP recommends against logging the session key. You must ensure that the session key is added after the request is logged.
The ASVS also recommends expiring your sessions after inactivity and also after a fixed period, regardless of activity. Ring sessions do not do this automatically (the builtin mechanism has no notion of expiration) and the default implementations of session stores will store and accept sessions indefinitely. A simple middleware will do the trick of expiring them in both cases:
(defn wrap-expire-sessions [hdlr & [{:keys [inactive-timeout
hard-timeout]
:or {:inactive-timeout (* 1000 60 15)
:hard-timeout (* 1000 60 60 2)}}]]
(fn [req]
(let [now (System/currentTimeMillis)
session (:session req)
session-key (:session/key req)]
(if session-key ;; there is a session
(let [{:keys [last-activity session-created]} session]
(if (and last-activity
(< (- now last-activity) inactive-timeout)
session-created
(< (- now session-created) hard-timeout))
(let [resp (hdlr req)]
(if (:session resp)
(-> resp
(assoc-in [:session :last-activity] now)
(assoc-in [:session :session-created] session-created))
resp))
;; expired session
;; block request and delete session
{:body "Your session has expired."
:status 401
:headers {}
:session nil}))
;; no session, just call the handler
;; assume friend or other system will handle it
(hdlr req)))))
Set the HttpOnly attribute on the session cookie. Very important for preventing stealing of session ids from XSS attacks.
Do not set the Domain
attribute,
and do set the Path if you want something more restrictive than /
(the Ring session default).
Do not set the Expire and Max-Age attributes. Setting them makes the browser store the session id on disk, which simply expands the number of ways an attacker can get ahold of it.
Change the session cookie name to something utterly generic, like "id". You don't want to leak more information than necessary about how your sessions work.
Use HTTPS if you can and set the Secure attribute of the cookie.
Do not use in-cookie sessions. In-memory are good but they can't scale
past one machine. carmine
has a redis-based session implementation.
Summary: Here's how I use Ring sessions (with carmine
) based on
these OWASP recommendations.
(session/wrap-session
(wrap-expire-sessions
handler
{:inactive-timeout 500
:hard-timeout 3000})
{:cookie-name "id"
:store (taoensso.carmine.ring/carmine-store redis-db
{:expiration-secs (* 60 60 15)
:key-prefix ""}) ;; leak nothing!
:cookie-attrs {:secure true :httponly true}})
A3 Cross-Site Scripting (XSS)
Whenever text from one user is shown to another user, there is the
potential for injecting code (HTML, JS, or CSS) that is run in the
victim's browser. Imagine if Facebook allowed any HTML in the post
submission form. A malicious user could add a <script>
tag with some
keystroke logging code. Anybody who viewed that post in their feed
would also get the key logger installed. That would be bad.
XSS is common because of how easy it is to make an app that stores user input (from a form post) in a database, then constructs the page out of stuff from the database. If you're not extremely careful, you could create a place where people can exploit each other.
The solution is to only use scrubbed or escaped values to build HTML pages. Because HTML pages can include different languages (HTML, CSS, JS), text needs to be scrubbed differently in each context. OWASP has a set of rules to follow which will guarantee XSS prevention.
hiccup.util/escape-html
(also aliased as hiccup.core/h
) will escape
all dangerous HTML characters into HTML entities. JS and CSS still need
to be handled, and rules for HTML attributes need to be followed.
If you want to allow some HTML elements, you will need to do a complex scrub. Luckily, Google has a nice Java library that sanitizes HTML. Use it.
Summary: Validate and scrub input from the user and scrub/escape text on output.
A4 Insecure Direct Object References
This one is a biggie: each handler has to do authentication. Does the particular logged in user have access to the resources requested? There's no way to automate this with a middleware. But having some system is better than doing it ad hoc each time. Remember: an attacker can construct any URL, including URLs with a database key in it. Don't assume that just because a request contains a key, the user must have the rights to it.
Summary: Always check the authority of the requesting session before performing an action.
A5 Security Misconfiguration
This is about keeping your software up to date and making sure the settings of all software makes sense.
A6 Sensitive Data Exposure
Having data is risky. Don't let it leak out.
A7 Missing Function Level Access Control
Use an authorization system (Friend) and audit the roles used for access control.
A8 Cross-Site Request Forgery (CSRF)
Let's imagine you have a bank account at Bank of Merica. You just checked your balance and didn't log out. Then you go to some public forum, where someone has posted a cool file. There's a big download button. You click it, and the next thing you know, you're on your bank page and all of your money has been transfered out of your account.
What happened?
The download button said "Download" but it was really a form submit button. The form had hidden fields "to-account", and "amount". The action of the form was "http://www.bankofmerica.com/transfer-money". By clicking that button, the form was posted to the bank, and because you were just logged in, oops, it transfered all your money away.
The solution is that you only want to accept form posts that come directly from your site, which you control. You don't want some random person to convince people to click on other sites to be able to transfer people's money like that.
There are several possible solutions. One approach is to add a secret to the session and also insert that secret into every form. That is the approach taken by the ring-anti-forgery library.
The solution that I like is to do a double-submit. This means you submit a secret token in the cookie (sent with each web request) and in a hidden field in the form. The server confirms that the cookie and the hidden field match. But the hidden field in the form is added by a small Javascript script which reads it from the cookie. Browsers don't allow Javascript to read cookies from other sites, so you guarantee that they form was posted from your site.
There are three parts to the solution.
- Install a secret token as a cookie.
- Install a script to add the hidden field to all forms.
- Check that the field matches the cookie on POSTs.
Here is some code to do 1 and 3.
(defn is-form-post? [req]
(and (= :post (:request-method req))
(let [ct (get-in req [:headers "content-type"])]
(or (= "application/x-www-form-urlencoded" ct)
(= "multipart/form-data" ct)))))
(defn csrf-tokens-match? [req]
(let [cookie-token (get-in req [:cookies "csrf"])
post-token (get-in req [:form-params "csrf"])]
(= cookie-token post-token)))
(defn wrap-csrf-cookie [hdlr]
(fn [req]
(let [cookie (get-in req [:cookies "csrf"]
(str (java.util.UUID/randomUUID)))]
(assoc-in (hdlr req) [:cookies "csrf"] cookie))))
(defn wrap-check-csrf [hdlr]
(fn [req]
(if (is-form-post? req)
(if (csrf-tokens-match? req)
;; we're safe
(hdlr req)
;; possible attack
{:body "CSRF tokens don't match."
:status 400
:headers {}})
;; we don't check other requests
(hdlr req))))
The Javascript should be something like this:
(def csrf-script "(function() {
var cookies = document.cookie;
var matches = cookies.match(/csrf=([^;]*);/);
var token = matches[1];
$('form').each(function(i, form) {
if(form.attr('method').toLowerCase() === 'post') {
var hidden = $('<input />');
hidden.attr('type', 'hidden');
hidden.attr('name', 'csrf');
hidden.attr('value', token);
form.append(hidden);
}
})
}());")
You should add it to all HTML pages. Note that this example script
requires jQuery. Put it right before the </body>
.
[:script csrf-script]
The nice thing about this solution is that it is strict by default.
If you don't include the script, form posts won't work (assuming
wrap-check-csrf
is in your middleware stack).
Summary: CSRF attacks take advantage of properties of the browser (instead of properties of your server), so their defense can largely be automated.
A9 Using Components with Known Vulnerabilities
Software with known vulnerabilities is easily attacked using scripts. You should ensure that all of your software is up-to-date.
A10 Unvalidated Redirects and Forwards
One common pattern for login workflow is to have a query parameter that contains the url to redirect to. Since it's a user parameter, it's open to the world and could be a doorway for attackers.
For example, let's say someone sends an email to someone asking them to log in to their bank account. In it, there's this link:
http://www.bankofmerica.com/login?redirect=http://attackersite.com
What happens when they click? They see the legitimate site of their bank, which they trust. But it redirects them to the attacker's site, which has been designed to look like the bank site. The user might miss this change of domains and unwittingly reveal private information.
What can you do?
OWASP recommends never performing redirects, which is impractical. The next best thing is to never base the redirect on a user parameter. This would work, but puts a lot of trust in the developers and security auditors to check that the policy is enforced. My preferred solution allows redirects that conform to a whitelist of patterns.
(def redirect-whitelist
[#"https://www.bankofmerica.com/" ;; homepage
#"https://www.bankofmerica.com/account" ;; account page
...
])
(defn wrap-authorized-redirects [hdlr]
(fn [req]
(let [resp (hdlr req)
loc (get-in resp [:headers "Location"])]
(if loc
(if (some #(re-matches % loc) redirect-whitelist)
;; redirect on our whitelist, it's ok!
resp
;; possible attack
(do
;; log it
(warning "Possible redirect attack: " loc)
;; change redirect back to home page
(assoc-in resp [:headers "Location"] "https://www.bankofmerica.com/")))
resp))))
Summary: Redirect attacks can largely be avoided by checking the redirect URL against a whitelist.
Conclusion
Web security is hard. It takes education and vigilance to keep our servers secure. Luckily, the main security flaws of the web are well-understood and well-documented. However, this is only half of the work. These need to be translated into Clojure either as libraries and simply as "best practices". Further, these libraries and practices need to be discussed and kept top-of-mind.