diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2008-07-28 12:26:59 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2008-07-28 12:33:24 +0100 |
commit | 6e754551254a8cc64e034163f5d0dc155b450388 (patch) | |
tree | e697e85d8699654f04a790e5dc323c7007e87e31 /railties/doc/guides/securing_rails_applications | |
parent | 10d9fe4bf3110c1d5de0c6b509fe0cbb9d5eda1d (diff) | |
download | rails-6e754551254a8cc64e034163f5d0dc155b450388.tar.gz rails-6e754551254a8cc64e034163f5d0dc155b450388.tar.bz2 rails-6e754551254a8cc64e034163f5d0dc155b450388.zip |
Merge docrails changes
Diffstat (limited to 'railties/doc/guides/securing_rails_applications')
4 files changed, 251 insertions, 0 deletions
diff --git a/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt b/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt new file mode 100644 index 0000000000..b237c4a17f --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt @@ -0,0 +1,86 @@ +== Creating records directly from form parameters == + +=== The problem === + +Let's say you want to make a user registration system. Your users table looks like this: + +------------------------------------------------------- +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name VARCHAR(20) NOT NULL, -- the login name + password VARCHAR(20) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT "User", -- e.g. "Admin", "Moderator, "User" + approved INTEGER NOT NULL DEFAULT 0 -- is the registered accound approved by the administrator? +); + +CREATE UNIQUE INDEX users_name_unique ON users(name); +------------------------------------------------------- + +The registration form: + +[source, xhtml] +------------------------------------------------------- +<form method="post" action="http://website.domain/user/register"> + <input type="text" name="user[name]" /> + <input type="text" name="user[password]" /> +</form> +------------------------------------------------------- + +The easiest way to create a user object from the form data in the controller is: + +[source, ruby] +------------------------------------------------------- +User.create(params[:user]) +------------------------------------------------------- + +But what happens if someone decides to save the registration form to his disk and play around with adding a few fields? + +[source, xhtml] +------------------------------------------------------- +<form method="post" action="http://website.domain/user/register"> + <input type="text" name="user[name]" /> + <input type="text" name="user[password]" /> + <input type="text" name="user[role]" value="Admin" /> + <input type="text" name="user[approved]" value="1" /> +</form> +------------------------------------------------------- + +He can create an account, make himself admin and approve his own account with one click. + +=== The solution === + +Active Record provides two ways of securing sensitive attributes from being overwritten by malicious users that change the form. The first is `attr_protected` that denies mass-assignment the right to change the named parameters. + +Using `attr_protected`, we can secure the User models like this: + +[source, ruby] +------------------------------------------------------- +class User < ActiveRecord::Base + attr_protected :approved, :role +end +------------------------------------------------------- + +This will ensure that on doing `User.create(params[:user])` both `params[:user][:approved]` and `params[:user][:role]` will be ignored. You'll have to manually set them like this: + +[source, ruby] +------------------------------------------------------- +user = User.new(params[:user]) +user.approved = sanitize_properly(params[:user][:approved]) +user.role = sanitize_properly(params[:user][:role]) +------------------------------------------------------- + +=== Allowing instead of protecting === + +If you're afraid you might forget to apply `attr_protected` to the right attributes before making your model available to the cruel world, you can also specify the protection in reverse. You simply allow access instead of deny it, so only the attributes named in `attr_accessible` are available for mass-assignment. + +Using `attr_accessible`, we can secure the User models like this: + +[source, ruby] +------------------------------------------------------- +class User < ActiveRecord::Base + attr_accessible :name, :password +end +------------------------------------------------------- + +This description has exactly the same affect as doing `attr_protected :approved, :role`, but when you add new attrbutes to the User model, they'll be protected by default. + diff --git a/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt b/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt new file mode 100644 index 0000000000..56b3940511 --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt @@ -0,0 +1,64 @@ +== Cross Site Scripting (CSS/XSS) == + +=== The problem === + +Many web applications use session cookies to track the requests of a user. The cookie is used to identify the request and connect it to the session data (the `session` method in Rails). Usually the session contains a reference to the user that is currently logged in, e.g. the id of a User object. + +Cross Site Scripting is a technique for ``stealing'' the cookie from another visitor of the website, and thus stealing the login. Cookies are only available from the domain where the were originally created. The easiest way to get access to the cookie is to place a specially crafted piece of JavaScript code on the website; the script can read the cookie of a visitor and send it to the attacker, e.g. by transmitting the data as an URL parameter to another website. +2.2 Example of an attack + +A site with the following Eruby template is vulnerable to XSS: + +------------------------------------ +<%= params[:text] %> +------------------------------------ + +The 'text' parameter is directly included in the page, so it is possible to insert JavaScript code by setting the parameter to something like + +[source, xhtml] +------------------------------------ +<script>alert(document.cookie)</script> +------------------------------------ + +After url-encoding the script (e.g. with `CGI.encode`) the attacker can put it into an URL and make his victim open the URL in the browser. + +If you open the URL + +------------------------------------ +http://website.domain/controller/action?text=%3Cscript%3Ealert%28document.cookie%29%3C%2Fscript%3E +------------------------------------ + +you will get a popup window with your session cookie. Instead of opening the popup the script could as well send the session id to another server. + +=== How to protect your application === + +Obviously the key to a successful attack is the possibility to place JavaScript on a website that can receive the session cookie. This can be a blog with user comments, a web forum or a Wiki. How can you protect against it? Strictly convert HTML meta characters (`<` and `>`) to the equivalent HTML entities (`<` and `>`) in every string that is rendered in the website. This will ensure that, no matter what kind of text an attacker enters in a form or attaches to an URL, the browser will always render it as plain text and never interpret any HTML tags. This is a good idea anyway, as a user can easily mess up your layout by leaving tags open. If you want the user to be able to format his texts, it is usually better to use a markup language like Textile, Markdown or RDoc. + +Rails provides the helper method `h` for HTML meta character conversion in Views. + +Example: + +------------------------------------ +<%=h post.subject %> +<%=h post.text %> +------------------------------------ + +You should accustom to using `h` for any variable that is rendered in the view, even if you think you can trust it to be from a reliable source. + +=== XSS attacks using an echo service === + +The echo service is a service running on TCP port 7 that returns back everything you send to it. On Debian it is active by default. Now how can this be a security problem for a web application? + +If the server of the target website 'target.domain' is running an echo service, the attacker could create a form like the following on his own website: + +[source, xhtml] +------------------------------------ +<form action="http://target.domain:7/" method="post"> + <input type="hidden" name="code" value="some_javascript_code_here" /> + <input type="submit" /> +</form> +------------------------------------ + +If he makes someone who has a session cookie on the target website submit this form, the content of the hidden field is sent to the echo server at port 7 – and returned. If the browser decides to display the returned data as HTML (some versions of IE do), it will execute the JavaScript code; and because the domain is 'target.domain' the session cookie is available for the script. + +This is rather a client-side issue, but to reduce the probability of a successful attack you should deactivate any echo services on the web server. However this does not provide full security, because there are also some other services (e.g. FTP, POP3) that can be used instead of the echo server. diff --git a/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt b/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt new file mode 100644 index 0000000000..b2cebbd311 --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt @@ -0,0 +1,14 @@ +Securing Rails applications +=========================== + +This manual describes common security problems in web applications and how +to avoid them with Rails. If you have any questions or suggestions, please +mail me at ror(at)andreas-s.net. + + +include::sql_injection.txt[] + +include::cross_site_scripting.txt[] + +include::creating_records_directly_from_form_parameters.txt[] + diff --git a/railties/doc/guides/securing_rails_applications/sql_injection.txt b/railties/doc/guides/securing_rails_applications/sql_injection.txt new file mode 100644 index 0000000000..51a0520b39 --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/sql_injection.txt @@ -0,0 +1,87 @@ +== SQL Injection == + +=== The problem === + +SQL injection is the #1 security problem in many web applications. How does it work? If the web application includes strings from unreliable sources (usually form parameters) in SQL statements and doesn't correctly quote any SQL meta characters like backslashes or single quotes, an attacker can change `WHERE` conditions in SQL statements, create records with invalid data or even execute arbitrary SQL statements. + +=== How to protect your application === + +If you only use the predefined ActiveRecord functions (attributes, save, find) without writing any conditions, limits or SQL queries yourself, ActiveRecord takes care of quoting any dangerous characters in the data for you. + +If you don't, you have to make sure that strings for arguments that are directly used to build SQL queries (like the condition, limit and sort arguments for `find :all`) do not contain any SQL meta characters. + +=== Using arbitrary strings in conditions and SQL statements === + +==== Wrong ==== + +Imagine a webmail system where a user can select a list of all the emails with a certain subject. A query could look like this: + +[source, ruby] +--------------------------------------------------------------------------- +Email.find(:all, "owner_id = 123 AND subject = '#{params[:subject]}'") +--------------------------------------------------------------------------- + +This is dangerous. Imagine a user sending the string `\' OR 1 \--` in the parameter 'subject'; the resulting statement will look like this: + +[source, ruby] +--------------------------------------------------------------------------- +Email.find(:all, "owner_id = 123 AND subject = '' OR 1 --'") +--------------------------------------------------------------------------- + +Because of ``OR 1'' the condition is always true. The part ``\--'' starts a SQL comment; everything after it will be ignored. The result: the user will get a list of all the emails in the database. + +(Of course the 'owner_id' would have to be inserted dynamically in a real application; this was omitted to keep the examples as simple as possible.) + +==== Correct ==== + +[source, ruby] +--------------------------------------------------------------------------- +Email.find(:all, ["owner_id = 123 AND subject = ?", params[:subject]]) +--------------------------------------------------------------------------- + +If the argument for find_all is an array instead of a string, ActiveRecord will insert the elements 2..n of the array for the `?` placeholders in the element 1, add quotation marks if the elements are strings, and quote all characters that have a special meaning for the database adapter used by the `Email` model. + +If you don't like the syntax of the array, you can take care of the quoting yourself by calling the `quote_value` method of the model class. You have to do this when you use `find_by_sql`, as the Array argument doesn't work there: + +[source, ruby] +--------------------------------------------------------------------------- +Email.find_by_sql("SELECT * FROM email WHERE owner_id = 123 AND subject = #{Email.quote_value(subject)}") +--------------------------------------------------------------------------- + +The quotation marks are added automatically by `Email.quote_value` if the argument is a string. + +=== Extracting queries into model methods === + +If you need to execute a query with the similar options in several places in your code, you should create a model method for that query. Instead of + +[source, ruby] +--------------------------------------------------------------------------- +emails = Email.find(:all, ["subject = ?", subject]) +--------------------------------------------------------------------------- + +you could define the following class method in the model: + +[source, ruby] +--------------------------------------------------------------------------- +class Email < ActiveRecord::Base + def self.find_with_subject(subject) + Email.find(:all, ["subject = ?", subject]) + end +end +--------------------------------------------------------------------------- + +and call it like this: + +[source, ruby] +--------------------------------------------------------------------------- +emails = Email.find_with_subject(subject) +--------------------------------------------------------------------------- + +This has the advantage that you don't have to care about meta characters when using the function `find_with_subject`. Generally you should always make sure that this kind of model method can not break anything, even if it is called with untrusted arguments. + +.ActiveRecord automatically generates finder methods +NOTE: The `find_with_subject` method given in the above example is actually redundant. +ActiveRecord automatically generates finder methods for columns. In this case, ActiveRecord +automatically generates the method `find_by_subject` (which works exactly like the `find_with_subject` +method given in the example). `find_with_subject` is only given here as an example. In general, +you should use ActiveRecord's auto-generated finder methods instead of writing your own. |