Securing Web applications



  • Today's story brought up an interesting question. What are the important ways to secure a web application? I'm currently developing a web app as a side project right now, and I'd like to learn some ways hackers can get around security.

    For example for the SQL injection problem mentioned in the article:

    "SQL Injection. By using single-quote characters, attackers were able to easily "inject" SQL code. For example, typing "' OR '' = '" in the Password textbox would result in a query that asked for users "WHERE Password='' OR '' = ''." Because an empty string ('') is always equal to itself, this would always return a user."

    I store an md5 of the user's password in the database. When checking to see if the user used the correct password I just md5 the submitted password and compare it to the one in the database. Therefore, SQL injection is not possible. Is this an efficient way to get around this vulnerability?

    What are some other ways that a web developer can do to further secure an application? 

     



  • Do not query an SQL database by concatenating bits of query with bits of user input.

    <font size="+2">DO NOT</font>query an SQL database by concatenating bits of query with bits of user input.

    Ever.

     

    This is why we have prepared queries with parameter marks. Use them.
     



  • For PHP look at PEAR::DB and PEAR::MDB2 for parameterized queries. It will take all your user end data an sanitize it for insertion and handle that for you as well. Of course also make sure numbers are numbers and the dates are dates, or in other words validate your data.



  • The best way to secure against SQL injection is to never concatenate data, and secure it at the database access point. I use the following method to secure against SQL injection when I'm using a database that doesn't support prepared queries: (php, sorry):

    public function parsedQuery( $sql, array $params = null )
    {
      // ...
    

    if ( !is_null( $params ) )
    $sql = preg_replace( '/@(\w+)/e', '$this->encodeSqlValue( $params['\1'] )', $sql );

    // ...
    }

    public function encodeSqlValue( $value )
    {
    switch( true )
    {
    case is_null( $value ): return 'NULL';
    case is_int( $value ): return $value;
    case is_float( $value ): return sprintf( '%F', $value );
    case is_bool( $value ): return $value ? 1 : 0
    default: return ''' . $this->connection->real_escape_string( $value ) . ''';
    }
    }

    called as

    $db->parsedQuery(
      'SELECT a, b, c FROM tbl WHERE fld1 = @foo AND fld2 = @bar',
      array( 'foo' => $somevar, 'bar' => $someothervar )
    );


  • Damn edit limit.

    Other things:

    - Don't think that because you can't do it in a browser, you can't do it. An HTTP request can be trivially hand crafted in any way imaginable. POST/cookie data is just as viable to be tampered as the querystring.
    - Don't store important state data in cookies, ever. Store an identifier that isn't easily guessable (a random hash) in the cookie, and use that to look up the data on the serverside.
    - Always encode any output that originally came from users, even if it's in apparently harmless places like an input value. Don't rely on functions that try to strip HTML. The list of tricks to get around those is enormous. Always encode < > and &. A single breach is enough to compromise all your security.
    - Salt your passwords, at least with a random number stored alongside the hash. MD5 hashes are fairly easily broken these days. If you really want secure, use a nonce. Encode the hash in Javascript if you can. There are plenty of libraries that can do that these days.



  • @krizo said:

    I store an md5 of the user's password in the database. When checking to see if the user used the correct password I just md5 the submitted password and compare it to the one in the database. Therefore, SQL injection is not possible.

    I'm a bit dazed from all the cold coffee + cream last night and maybe I'm misreading a word here or there, but the injection is still perfectly possible. The MD5 doesn't do anything at all if something like  OR 1 = 1 is added.

    consider:

    if (plainValue = getValueFromStorage()) {
       //access!
    }

    After the injection, this amounts to:

    if (plainValue = getValueFromStorage() || true) {
       //access!
    }
     

    And this code is exactly the same: 

    if (reallySecureHashedValue = getHashedValueFromStorage() || true) {

       //access!

    }



  • @dhromed said:

    @krizo said:

    I store an md5 of the user's password in the
    database. When checking to see if the user used the correct password I
    just md5 the submitted password and compare it to the one in the
    database. Therefore, SQL injection is not possible.

    I'm
    a bit dazed from all the cold coffee + cream last night and maybe I'm
    misreading a word here or there, but the injection is still perfectly
    possible. The MD5 doesn't do anything at all if something like  OR 1 = 1 is added.

    In this case no, it's not, assuming that he's not receiving a hash from the outside, but rather the password.

    md5( $_GET['please_exploit_me_through_the_ass'] ) == '<an MD5 hash string, regardless of what's in there>';
     



  • @Sunstorm said:


    - Salt your passwords, at least with a random number stored alongside the hash. MD5 hashes are fairly easily broken these days. If you really want secure, use a nonce.

    A salt is a random string used to initialise a hash to defend against precomputed lookup table attacks (rainbow tables and suchlike). A nonce is a random string added to a hashed message to defend against replay attacks. There's no real difference between them, except for context - they're just two different ways in which adding a random string to a message can be useful.



  • @Sunstorm said:

    I use the following method to secure against SQL injection when I'm using a database that doesn't support prepared queries:

    What, mysql 3.x or something? Every SQL DBMS worthy of the name supports prepared queries. Are you using the php builtin database functions? Don't, they suck. 



  • Other stuff: 

    1. Never trust anything from the user.  This includes the query parameters themselves.  Users are 1 part smart and malicious, 1 part smart and honest, and approximately 20 parts stupid.  These ratios may change depending on the purpose of the site.
    2. It's now off by default, but in PHP, never use register_globals.  Get the variables you need from the place you expect them, be it $_GET, $_POST, $_SESSION, &c.
    3. Never store secure data in hidden input fields. Viewing source is so easy.
    4. Rely on sessions to send important information from page to page.  Also remember that sessions aren't perfect, either.  The session ID can potentially be stored indefinitely on the user side.  Therefore, make sure sessions are invalidated after a short period of time.  What constitutes a short period of time depends on how people will use your site.
    5. Never transmit secure information in the clear.  Hash passwords on the client side, and use HTTPS for important systems.
    6. Turn off detailed error messages on production systems.  Just use a default error 500 page.  Log the errors on the server side.
    There's plenty more, but I'm too tired to think of them right now.


  • @Sunstorm said:

    @dhromed said:

    @krizo said:

    I store an md5 of the user's password in the
    database. When checking to see if the user used the correct password I
    just md5 the submitted password and compare it to the one in the
    database. Therefore, SQL injection is not possible.

    I'm a bit dazed from all the cold coffee + cream last night and maybe I'm misreading a word here or there, but the injection is still perfectly possible. The MD5 doesn't do anything at all if something like  OR 1 = 1 is added.

    In this case no, it's not, assuming that he's not receiving a hash from the outside, but rather the password.

    md5( $_GET['please_exploit_me_through_the_ass'] ) == '<an MD5 hash string, regardless of what's in there>';
     

    Right, yes. :)
     



  • @asuffield said:

    What, mysql 3.x or something? Every SQL DBMS worthy of the name supports prepared queries. Are you using the php builtin database functions? Don't, they suck. 

    The mysql extension for PHP (which is what I imagine you call builtin functions) doesn't support prepared queries. It has been deprecated by the mysqli extension, but I've been faced with many a hosting server that refused to upgrade. It's better security than nothing.



  • @Sunstorm said:

    @dhromed said:

    @krizo said:

    I store an md5 of the user's password in the
    database. When checking to see if the user used the correct password I
    just md5 the submitted password and compare it to the one in the
    database. Therefore, SQL injection is not possible.

    I'm
    a bit dazed from all the cold coffee + cream last night and maybe I'm
    misreading a word here or there, but the injection is still perfectly
    possible. The MD5 doesn't do anything at all if something like  OR 1 = 1 is added.

    In this case no, it's not, assuming that he's not receiving a hash from the outside, but rather the password.

    md5( $_GET['please_exploit_me_through_the_ass'] ) == '<an MD5 hash string, regardless of what's in there>';
     

     

    Yes, that is what I do. Thanks for the info, guys.  



  • I should also note that I kind of do the same thing with the username as well to avoid injection. This is a rough query of what I do for the query.

    $query = sprintf( "SELECT * FROM users WHERE username_enc='%s' AND password='%s'", md5( $_GET['username']), md5($_GET['password'] ) );
     For any other inputs that I get from a form I use the mysql_escape_string function in PHP.

     



  • @krizo said:

    I should also note that I kind of do the same thing with the username as well to avoid injection. This is a rough query of what I do for the query.

    $query = sprintf( "SELECT * FROM users WHERE username_enc='%s' AND password='%s'", md5( $_GET['username']), md5($_GET['password'] ) );

     For any other inputs that I get from a form I use the mysql_escape_string function in PHP.

    That seems unnecessary. How would you then retrieve the username of the person that is logged in?

    At most, you could make up the password hash from both the username and the password's MD5, with some salt in between. That would be like a per-user salt, of sorts.



  • @Sunstorm said:

    That seems unnecessary. How would you then retrieve the username of the person that is logged in?

     It appears that he probably has two fields, one for the MD5'd name and one for the plain name, looking at the query.  (notice the _enc suffix on the table name.)

     That said, half the stuff in this post are serious WTFs on their own.

     MD5'ing a password or username or anything else before substituting into the taw SQL is, technically, a protection against SQL injection.  It's still massively stupid.  You should be escaping the data every time, using the database driver's native escaping routines (be that MDB2::quote for PHPers or prepared statement placeholders for everyone).  If you fail to do this, you will fail to build the habit of quoting your input, and you're going to end up accidentally creating SQL injection holes.  Every single piece of data, every single time, must be quoted.

     Another advantage to doing it everytime is that it then makes it possible to write simple tools to statically check your code for mistakes.  You can run a small Perl script to ensure that all of your SQL is being protected.  If you do silly things like

     $int = intval($_GET['variable'])

    $db->query("SELECT * FROM foo WHERE bar=$int");

    Then you completely lose the ability to run quick security checks.  Sure, that code is safe, but it's much harder to know that.  Especially if there is a bunch of code between the assignment to $int and the SQL statement.

    The term that applies here is "code locality."  You move your SQL protection code (the intval call) out of the actual SQL itself, so now it's harder to look at the code and know that it's safe.  It makes analyzing the code harder, and if you get in the habit of writing that kind of code, you'll be less likely to notice when there is an actual hole.

    Placeholders are best, because they leave no room for error.  You can look at every single call to $db->query (or whatever other method your language/library uses for SQL queries), see that the SQL is a single static string with no concatenation or substitution (again, a script can even do that for you), and you are 100% sure that the values are being substituted appropriately.

    If for whatever reason you can't use placeholders, at least be sure you always always calling the proper quoting mthod on your SQL, e.g

    $db->query("SELECT * FROM foo WHERE bar=".$db->quote($int));

    Not as pretty, but it's still possible to run a checker tool, and you can look at a query and instantly notice if a value if missing the $db->quote that you know always be there.

    The only time you should be concatenating anything without an explicit quote is when you're actually building the SQL piece by piece.  Best to avoid that kind of stuff, but that's not always possible.  Just keep good habits on you and clearly documentation (a short comment will usually do the trick) what code is doing when you are concatenating SQL without quoting.
     



  • @Sunstorm said:

    @dhromed said:

    @krizo said:

    I store an md5 of the user's password in the database. When checking to see if the user used the correct password I just md5 the submitted password and compare it to the one in the database. Therefore, SQL injection is not possible.

    I'm a bit dazed from all the cold coffee + cream last night and maybe I'm misreading a word here or there, but the injection is still perfectly possible. The MD5 doesn't do anything at all if something like  OR 1 = 1 is added.

    In this case no, it's not, assuming that he's not receiving a hash from the outside, but rather the password.

    md5( $_GET['please_exploit_me_through_the_ass'] ) == '<an MD5 hash string, regardless of what's in there>';
     

    That protects the password, but it doesn't protect the username.

    If this is your query:

    select * from users where username = 'username' and password_enc = 'hash'

    what happens then I type the following into the username field...

    ' or 1=1; --

     

    To quote (pun intended) elanthis, you MUST filter every piece of user input individually.  Not just the ones you think are failure-points.



  • @RaspenJho said:

    To quote (pun intended) elanthis, you MUST filter every piece of user input individually.  Not just the ones you think are failure-points.

    Indeed. Trust nothing.

    I've seen a webshop that stored the total value of all products in a hidden field in the form. shivers (Also note that addslashes is not enough, google around for "addslashes vs mysql_real_escape_string" for details)



  • @krizo said:

    I should also note that I kind of do the same thing with the username as well to avoid injection. This is a rough query of what I do for the query.

    $query = sprintf( "SELECT * FROM users WHERE username_enc='%s' AND password='%s'", md5( $_GET['username']), md5($_GET['password'] ) );
     For any other inputs that I get from a form I use the mysql_escape_string function in PHP.

     

     

     

    I could be wrong, but I thought that the mysql_escape_string function has been deprecated in favor of the newer mysql_real_escape_string function.

    http://us.php.net/function.mysql-real-escape-string 

     

    also, the new hash function supports more complicated hashing algorithms like SHA256 among others, if you're worried about using MD5 hash, which I would be. 



  • @kaamoss said:

     

    I could be wrong, but I thought that the mysql_escape_string function has been deprecated in favor of the newer mysql_real_escape_string function.

     

    Unrelated, I like the "naming scheme". What's the next one? mysql_really_real_escape_string() ?



  • @PSWorx said:

    Unrelated, I like the "naming scheme". What's the next one? mysql_really_real_escape_string() ?

     

    Ha, I think it might be more like mysql_double_real_escape_string()

    Since double and real are both floating point numbers, and a double is even more precise! :) 



  • I'm actually working on an AJAX auth system currently with php, and I have a quick question that maybe someone here could help me with.

    Right now, users passwords are stored as sha256 hashes using php's hmac_hash function as well as a unique salt for the user, I assume that when an ajax user makes an authentication request I should send the client the unique salt for that user and a 1 time challenge key. The client then performs two HMACs.

    First, the password is hashed with the salt, and then the resulting value is hashed together with the one time challenge.  The client then sends this value along with the username to the server for authentication. Is this sufficiently secure? I'm not storing any incredibly sensitive data, where using SSL would be reasonable, but I'd also like it to be as secure as possible. Do I even really need the salt, that protects against dictionary attacks, but I thought those attacks are usually more common with sha1 and md5? I don't know much about crypto so any advice would be welcomed.

     



  • @kaamoss said:

    Do I even really need the salt, that protects against dictionary attacks, but I thought those attacks are usually more common with sha1 and md5? I don't know much about crypto so any advice would be welcomed.

    Depends on how much salt you're willing to sprinkle around on the data. The old 2-character salts used for classic-style Unix password hashes were fine for a while, but now it's trivial to build a complete rainbow table of the full hash-space, including all possible salts.

    Dictionary attacks are possible against any authentication system you care to try. Whether the attack is practical or not depends on how secure you want to feel. Figure that anything you to do today will be practical by Monday, easy by Tuesday, and on cereal box decoder rings everywhere by Wednesday.

    As you state, you're not sending anything incredibly sensitive, and the data's going through an SSL pipe. So why bother with all the hashing? If you're worried about the PW being sniffed in transit, then SSL will take care of that for all but the most determined/well funded attackers.

    Just make sure both ends of the connection are secure: It's easier, BY FAR, to crack a server and sniff data after it's exited the SSL pipe than it is to try and intercept the data in flight. Also more practical for the attacker. Why try and hold up 50 random people on the street, when you can trivially rob the store they just spent some money in?



  • @kaamoss said:

    First, the password
    is hashed with the salt, and then
    the resulting value is hashed together with the one time challenge. 
    The client then sends this value along with the username to the server
    for authentication. Is this sufficiently secure?

    It's a start, but there's more to security than the basic protocol. All the cryptographic tricks in the world won't help you when there's a gaping hole somewhere else, and very few systems fail because of cryptographic errors.

     

    I'm not storing any incredibly sensitive data, where using SSL would be reasonable

    Is there any compelling reason why you can't use SSL? It takes about ten minutes to set up, and only on very highly loaded systems (thousands of concurrent users or more) are there any CPU usage concerns. It will protect against a wide variety of errors that you're otherwise likely to make.

    Do I even really need the salt, that protects against dictionary attacks

    You do need the salt. That is not the reason. The salt and nonce should be of a similar size to the hash; if you're using the printable ascii characters and a 256-bit hash, that means your salt should be about 40 characters long. Its purpose is to compensate for the fact that a typical password has a lot less than 2^256 bits of entropy in it, which otherwise opens various attacks.

     

    but I thought those attacks are usually more common with sha1 and md5?

    The hash function used is irrelevant, as long as it has no known exploits.


Log in to reply