Top 4 web security flaws you should fix right now
Hackers are getting far more sophisticated at stealing websites' databases, deploying tricks that are fairly hard to defend against at the web developers' level, such as so-called "social engineering" attacks which trick users into disclosing important passwords. Nevertheless, old-fashioned, preventable hacks still account for the vast majority of data breaches on the internet. VTech's recent loss of 4.8 million users' data through an SQL injection vulnerability shows how these security flaws remain common and devastating.
So here's the top 4 most common security problems and how to solve them.
-
Not using TLS/SSL
Problem:Go look at Troy Hunt's excellent post—there's a huge misconception among web users and web developers that encryption and SSL is merely a matter of keeping sensitive data secret. Far more importantly SSL guarantees a website's authenticity, since without it a hacker can replace content with their own to harvest passwords or infect visitors with malware. Distributed Denial of Service attacks are often performed by injecting javascript into random non-encrypted web traffic, which the Chinese government apparently did to censor political opponents. There are free exploit kits anyone can use to hijack traffic to any unencrypted site they want with just a click. Here's one.
Solution:
- Buy yourself an SSL certificate and upgrade your webhost to SSL. You can test the strength of your server's SSL implementation here.
- Since browsers attempt to go to http if https is not specified, you'll need to redirect them, but it's important to only redirect GET requests. POST,PUT, and DELETE should throw an error if the connection is not https, because you want a loud obnoxious failure whenever a web developer accidentally POSTs sensitive information to a non-encrypted URL (which differs by just one character, this is a common typo).
- You should also implement HSTS and set the
Strict-Transport-Security
response header to cause browsers to remember to use only SSL for future requests to that domain.
-
SQL injection
Problem: SQL injection happens when your app generates SQL statements based on user-supplied inputs like search terms to do database operations. Users could write things like "DROP TABLE" and screw up the data. This isn't just a problem when you are concatenating SQL statements either—if you pass user-data to as parameters to a stored procedure and call
EXEC
inside the procedure, users can still inject whatever SQL commands they want.Solution: 99 percent of you should be using an ORM like .Net's Entity Framework. I mean the linq-to-entities functionality—no passing concatenated queries to the ORM, and no stored procedures. You probably can't write SQL that will beat the ORM, and certainly not that would beat the ORM by enough to compensate for the extra development time manually writing SQL requires. As long as you stick to linq-to-entities you're safe from SQL injection. If you really need to write an SQL statement (look: you really don't) then make sure that the query is correctly parameterized. This can't be done at the SQL level (the
DECLARE @param1 nvarchar(300)
syntax is still vulnerable to injection)—you need to parameterize at the web-framework level. In .Net, this means adding parameters through the parameter collection of the SqlCommand object. -
Cross-site scripting
Problem: Frequently, websites display various kinds of user-generated data. Sometimes this is obvious—such as when readers leave comments on a blog post— but it can happen in less obvious ways such as when a user uses the search box, and the results page displays the user's search term. It can also happen when your ad network just doesn't give a crap. You have to bear in mind that users and third parties can input valid HTML, and when you display this HTML in the page, it will render as HTML. You don't want a commenter to be able to write a javascript tag that will then be executed on every reader's browser!
Solution: You need to properly escape all user inputs. For the most part, these are the characters that can cause trouble: &, <, >, ", ', /.
- The first thing to do is set your Content-Security-Policy header. With this header you can explicitly tell the browser not to allow clients or third parties to modify the DOM or inject scripts. Older browsers don't implement this, and it doesn't protect against vulnerabilities served from your own scripts, but it's a start.
- If you are using javascript to display text entered by a user on the page, then your javascript should escape the text before displaying it. A common way to do this is:
This will escape all HTML characters and render them as text rather than DOM elements. It's pretty standard, and basically what jQuery does. But you should be aware that it does not escape double quotes, and will therefore allow vulnerabilities whenever you insert user-generated text as an attribute, as in for example:function escapeHtml(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; };
. You could potentially use javascript's built-invar ele='<div ng-model="+escapeHTML(boo" some-attribute-you-dont-want="true)+"></div>';
encodeURIComponent
to escape data that will be stored in attributes, since human-readability isn't required. - A lot of user-content won't be added to the page using javascript though—reader comments, for example, will generally be served right up with the HTML from the server, so escaping in javascript simply won't catch it before it renders in user's browsers. You need to escape this stuff on the server, either when you save it to the database or before you render it into the HTML. The .Net framework has
HtmlEncode
andHtmlAttributeEncode
facilities built in for this purpose. As an alternative to escaping, you could instead reject requests that contain disallowed markup.
-
Cross-site request forgery
Problem:Web browsers send all the cookies for a domain along with each request to that domain, meaning that if a user has bob.com and alice.com open in their browser, a script on bob.com can POST (say, a purchase order) to alice.com and the browser will send the user's actual authentication cookie for alice.com along with this forged request. Thus merely checking that the requester has valid credentials is not enough because you cannot verify that the request originated on your site.
Solution:Use validation tokens for all request methods other than GET. The way this works is you have a pair of related randomly-generated numbers (should be different for every request), set one of them in a cookie for alice.com, and put the other one in the form to be POSTed back along with the request. Make sure that you disallow cross-origin requests (this is default in all browsers). Then the request is POSTed, check the form token against the cookie token—bob.com can't read the cookie token and therefore cannot produce a matching form token. Here's more on the validation token implementations in ASP.Net.