Intranet and Admin Security

These days the intranet is coming back. I heard it a couple of times: Our intranet is safe, there’s an authentication system and it can be accessed  by hosts from our local IP range only, but no, there are no further security measures. If someone manages to get in, he will be able to do and see a lot. And, yes, the bad guy can get in under certain circumstances.

Last year, for example, we have seen the first tailor-made Trojan which stole information from an intranet, the “Monster for employers” web site of Monster.com. These Trojans are very rare so far and the risk is quite low, but it’s certainly a possibility and an example of how the security of the client host is important, too.

 

XSS in your intranet
While special malware might be less likely for small intranets, XSS and CSRF are not. If your intranet application re-displays unsanitized user input from the extranet (user names, comments, spam reports, order addresses to name just a few uncommon places where I’ve seen malicious user input), the application will be vulnerable to XSS.
Already one single place in the intranet where the input has not been sanitized makes the entire application vulnerable. Vulnerable to what? Well, cookie stealing (YES, it’s a priviledged intranet cookie), content alteration to lure victims to a fake intranet where he might enter confidential information and so on. So I hope you have found a way to eliminate XSS everywhere in your app.

 

CSRF in your intranet
Yup, CSRF (Cross Site Request Forgery) is real, as well. It takes place in combination with XSS or the old-fashioned way: Send an HTML e-mail with CSRF to the victim or lure him to an infected web page. What is CSRF? Take a look at two examples which trigger a GET and POST action (.src is src):

<img .src=”http://intranet/project/1/delete” />

Generate this in Rails and you have a link to a POST action:

link_to(“To the survey”, “http://www.your-online-expense-tracker.com/account/delete”, :method => :post)

Now if the user views the page (for the image) or clicks the link, and he is logged in to the application (i.e. the cookie is set in the browser), the project or account will be deleted. Of course, the attacker has to know the URL structure, but most Rails URLs are quite straightforward or they’re publicly accessible like in the expense tracker application.
The attacker may even do 1,000 lucky guesses. The countermeasure in Rails is to include a token in each POST request that will be verified on the server. See the first post about this, but the csrf_killer plugin has been merged into Rails.

 

Administration
Now, you do have an administration interface for your application? How about its security? Did you take it more serious than the actual applications’ security? You should, because in most cases a security breach is more harmful here.

The common admin interface is like this: It’s located at www….com/admin, may be accessed only if the admin flag is set in the User model, re-displays user input and allows the admin to delete/add/edit a lot. Here are some thoughts about this:

  • As an admin interface is kind of an intranet, the same vulnerabilities as described above may be there. The attacker could steal an admin cookie (via XSS) or use CSRF to delete some users: www….com/admin/user/delete/2

 

  • It is always very important to think about the worst case: What if someone really got hold of my cookie or user credentials. You could introduce roles for the admin interface to limit the possibilities of the attacker.
    And how about special login credentials (other than the ones used for the public part of the app) for the admin controller and another password for very serious actions?

 

  • Does the admin really have to access the interface from everywhere in the world? Think about limiting the login to a bunch of source IP addresses.
  • Put the admin interface to a special sub-domain such as admin.application.com. This makes stealing an admin cookie from the usual domain impossible.
    This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa. Of course this precaution will be useless if the attacker got hold of the cookie from www.application.com and may simply copy the cookie string to make one for the admin.application.com host. Use two session tables (if you use the :active_record_store session store) or a seperate admin application for that.

The Tainted Edition

There has been a discussion about whether to untaint or not. A string becomes tainted in Ruby when it comes from an external source, for example. The standard Ruby method untaint marks it as untainted. Plugins such as SafeErb do not allow the programmer to output tainted strings (in Erb) in order to protect the application from XSS. This works fine in my experience (even in Rails 2) and won't need you to untaint manually too often.
 
There is a good comment in the mentioned discussion (and the author has written an interesting article about safe strings):
 
"What most tainting-based solutions seem to miss is that 'safeness' or 'taintedness' is not a property of a string but a relationship between the string and the contexts in which it is used."
 
Indeed, SafeErb escapes and untaints the strings according to one context only – HTML (SGML) – and that is the purpose of the plugin. But there are a lot of other contexts you will use strings in:
 
SQL
Thanks to clever helper methods, this is hardly a problem in most Rails applications. However, you have to follow the rules and remember the problem whenever you use tainted strings in plain SQL or uncommon places. Use sanitize_sql() to escape in this context.
 
SGML (HTML, XML, RSS…)
Escape strings using html_escape() or h() against XSS.
 
CSS
Beware of CSS Injection if you render style sheets using tainted strings. This was a hole the Samy worm exploited.
 
Update: Textile
Textile (by means of RedCloth) should be used only together with a white-list sanitizer like Rails' sanitize(). In addition to my previous post, it is possible to inject scripts in image titles like this: !bunny.gif(Bunny" onclick="alert(1))!
 
Will become:<p><img .src="bunny.gif" title="Bunny" onclick="alert(1)" alt="Bunny" onclick="alert(1)" /></p>
 
JavaScript
Do escape strings in a JS context as well if you render code using tainted strings.
 
JSON
There has been a problem with the to_json method earlier. Solved in Rails 1.2.5.
 
Rails
Do you use the params hash in Rails methods that accept hashes? How about redirect_to(params) or similar? Imagine what will happen when the hash contains a :host key! This is logic injection and can't be sanitized.
 
Log files
Someone could fill up your server's disk or manipulate your log files using log file injection. Remember to sanitize, filter (see filter_parameter_logging()) and truncate here.
 
Second level
Beware of command line injection.
 
All these different contexts raise the question when to sanitize a string, before or after storing it (if it is stored). Of course for some contexts (SQL, shell), it certainly has to be sanitized before being stored/processed. But what about the most important context in web applications, SGML? Will I save a sanitized string or the raw input? I prefer to sanitize (or escape actually) strings according to a context when using it in that context, because in a large application you will never know in which contexts you will use the input data in the future.
For performance reasons I store one untouched and one escaped (and even textiled and white-listed) version of the input. The raw version will be used if the user wants to edit the data, and the untainted version will be displayed.
 
There have come up quite a lot of plugins recently trying to automatically escape the user input data.

What I don't like about these solutions is that it might be seen as plug-n-play security to install and forget, because it does it all automatically. In my opinion security is a process and you have to reconcile for each and every string what it represents and how to escape it in this context. Of course an automatic escaper can be a great help, but only if you remember to sanitize it differently in an other context.

InvalidAuthenticityToken for in_place_editing?

There is a problem with InvalidAuthenticityToken errors that are raised in the methods for the in_place_editing plugin. This happens in Rails 2.0.2 (and possibly earlier versions). It's because there is no authenticity_token sent at all. You can apply this patch until there is a new version out.

If you have something like this:

<%= in_place_editor("title", {:url => url_for(:action => "update_title" …)}) %>

the update_title method will throw an error. Apply the patch to make it work.

SafeErb for Rails 2

Update: See this comment for how to fix problems with HelperMethods.

You might have noticed that the SafeErb plugin does not work in Rails 2 applications. That is because of old method signatures used in the plugin. The author has put up a blog post (in japanese) about a new version created by Aaron Bedra which points to this plugin installer (possibly replace http by svn):

./script/plugin install http://safe-erb.rubyforge.org/svn/plugins/safe_erb

The author has tested it with Rails 2.0.2 and it works fine. On my system however, it has problems with methods from the FormHelper (text_field and so on), most likely because of the output values in the value parameter. Does this happen on your system, as well? I hope to find a fix for that. Apart from that, the plugin works fine for Rails 2 applications.

Thanks to hurx for sending me his version. A happy new year to you all. 

Rails 1.2.6 security update

The rails core team has released ruby on rails 1.2.6 to address a bug in the fix for session fixation attacks (CVE-2007-5380). The CVE Identifier for this new issue is CVE-2007-6077. You should upgrade to this new release if you do not take specific session-fixation counter measures in your application.  

1.2.6 also fixes some regressions when working with has_many associations on unsaved ActiveRecord objects.

As with other 1.2.x releases, this is intended as a drop in upgrade for users of earlier versions in the 1.2 series.

 From the Rails log.

Rails 2.0 cookies (updated)

 

Rails 2.0 will include a new default session storage, the CookieStore (source source). What it does is store the clear text “marshalled” session object in a cookie which will be stored on the client side. Here is an example of a new cookie value:

BAh7BzoMdXNlcl9pZGkKIgpmbGFzaElDOidBY3Rpb25Db250cm9sbGVyOjpG%250
AbGFzaDo6Rmxhc2hIYXNoewAGOgpAdXNlZHsA–be9c1e802c6cf126c722c680
02ccbd5684a96dd9

Well, it is actually not clear text, but Base64 encoding. So everything you store in the session object can be seen by the client. This is especially bad if you store secrets in the session, because you thought they will be safe there. But it should be safe for saving an id, for example. Update: You can implement an EncryptedCookieStore very easily, so no one can see the information. But again, the password for this encryption has to be strong.
When the cookie travels back from the client to the application, the session data will be “unmarshalled” and made available by means of the session method (e.g. session[:user_id]).

The second part of the cookie (after the –) is a hash (HMAC-SHA1 by default), calculated of the session object along with the secret server side password. On the server side the session value will be checked against this hash, so no one can tamper with it…unless the attacker knows the password (secret) which is in your environment.rb:

  config.action_controller.session = {
    :session_key => ‘_cookies2_session’,
    :secret      => ‘secret-secret’  }

You already know what may happen: the user is able to locally brute-force the secret as he has the clear text and the hash of it (Update: whether or not this will be successful depends on the strength of the secret. A sufficiently long secret is safe.). A weak secret is a short one, or one included in a word list (there are some with 40 million entries). You can try to brute-force your secret with John the Ripper which is very fast (I checked over 105 million passwords in less than half an hour). Update: This means the secret should be a strong password (over 40 characters, no words from dictionaries). The latest Rails release forces you to use a secret with at least 30 characters, and it generates a sufficiently long secret by default.

Corey Benninger presented my book and the most dangerous attack methods for Rails at the OWASP AppSec conference in San Jose. And he wrote a nice Ruby script to crack a Rails cookie (with a too short secret!) here and here.

Update: Replay attacks might be another security issue with client-side cookies. If you store state (is_admin, points, money, amount_to_pay, whatever) in a session, it may be replayed. That means an attacker can resend the state in the cookie and thus possibly reduce a price, get more points or become an admin (if the cookie is from someone else who is an admin). Solutions for this are welcome. In any case, it is not a good idea to save such information in sessions, no matter whether it’s a client- or server-side session store.

There are different cookie stores, depending on your needs. If you use CookieStore, please set a very long secret (over 40 characters, no words from dictionaries), preferably some sort of hash as the Rails generator for new project proposes. Also, keep in mind that an attacker might use the knowledge from the cookie somewhere else in your application.

restful_authentication login security

There is a serious security leak in the restful_authentication plugin regarding the activation of an account. You can use it to log in w/o user credentials or impersonate someone else.

The “activate” method of the controller accepts an empty activation code parameter like this (depending on your routes):
http://localhost:3006/user/activate or http://localhost:3006/activate/?activation_code=

Which will create this SQL:
SELECT * FROM users WHERE (users.`activation_code` IS NULL) LIMIT 1

An attacker will be able to log in w/o password and use the first account found with an empty activation_code (activated users)!

This works for everyone in and outside the app, because you’d normally have a skip_before_filter :login_required, :only => [:activate] in the controller. Even if you don’t (rarely), registered users can impersonate someone else!

The author has been informed, and thankfully reacted with a new version of the plugin, replace the first line of the method with this (depending on your model names):

self.current_user = params[:activation_code].blank? ? :false : User.find_by_activation_code(params[:activation_code]) 

HTTP Authentication and Feed Security

In the context of looking for a secure way to send out feeds (RSS, Atom, …), I found several options:

  • Use basic access authentication to prompt a user name and password before granting access. This is supported by quite a lot feed readers and browsers (where you have to enter your credentials).
    • Advantage: Easy to use, Rails 2.0 provides a method for it: authenticate_or_request_with_http_basic
    • Disadvantage: Very insecure as the user name and password is send in plain text over the net (encoding is not encryption), everyone could sniff the network traffic and read the login credentials. Also, it is vulnerable to phishing.
  • Digest access authentication works basically the same, but encrypts the user name/password and other values using MD5 before sending.
    • Advantage: No clear text passwords will be transmitted, much more secure than basic authentication, but it is not intended to replace strong authentication
    • Disadvantages: The clear text password or the HA1 hash (MD5(user:realm:password)) must be stored on the server. If someone gets access to it, he may use rainbow tables to compute the password. This takes very very long for such long strings, but fortunately the user name and realm is known and the password can be found relatively fast, especially if it is a weak password from a dictionary.
      Also it is vulnerable to Man in the middle attacks.
    • Do not prompt the user name/password that the user uses to login to the web application. Maybe you set up another special user name which is allowed to view the feed, only. Keep in mind to turn off sessions for feeds (session :off). If you don't, and someone gets hold of the special user's credentials, he will have a valid session not only for the feed but (possibly) for the application, too.
    • There is a plugin for Rails: htdigest :user=>"maiha", :pass=>"812b1d067e9ce1e44f09215339e3cd69", :type=>:crypted
      or in a table: htdigest :class=>"FeedUser", :user=>"login", :pass=>"ha1"
    • Digest authentication is far better, but has its weaknesses. If you use it to authenticate access to a feed, it will be alright. Consider using a different user model though.
  • Update: Create a long URL which grants access to the feed. However, the URL becomes the password and can be seen in plain text traveling through the net. It's good over SSL, though.
  • This is an interesting solution. The plugin Greasemonkey is evil though, well if you don't know the particular script.
  • Use basic access authentication over SSL. Probably the best solution, as it encrypts the feed and the user name/password in transit. The tradeoff, however, is the slower speed

Any comments?

Rails 1.2.5 security release

There is another security release which addresses once again the to_json vulnerability. It now has a CVE. If you used to_json in a page you generate:

<script type="text/javascript">
var customers = <%= @customers.to_json %>;
</script>
 
you should upgrade to 1.2.5. Besides it fixes some bugs from 1.2.4.

Rails 1.2.4 Maintenance release, security

The release of Ruby on Rails 1.2.4 addresses some potential security issues, all users of earlier versions are advised to upgrade to 1.2.4.

The following issues have been addressed:

  • URL-based sessions are no longer enabled by default, as it allowed users to provide their session_id in the URL as well as cookies.  The functionality could be exploited by a malicious user to obtain an authenticated session.
    Use config.action_controller.session_options[:cookie_session_id_only] = false to re-enable it
  • Changed the JSON encoding algorithms to avoid potential XSS issues when using ActiveRecord::Base#to_json
  • Potential Information Disclosure or DoS with Hash#from_xml: Maliciously crafted requests to a Rails application could cause the XML parser to read files from the server's disk or the network. 1.2.4 removes this functionality entirely.