Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

Is this a secure way to prevent cookie tampering

by EvdB (Deacon)
on Jun 29, 2004 at 14:03 UTC ( [id://370480]=perlquestion: print w/replies, xml ) Need Help??

EvdB has asked for the wisdom of the Perl Monks concerning the following question:

I am creating a website which will cater for several clients, each of whom will have several users.

To keep life simple I will create a database for each client and all the relevant stuff will go in there. There will also be a master database that stores only the information that is needed to get the user to the right database at login.

The users will be given sessions (stored in cookies) which say not only who they are but also which database they should connect to. Once the database has been determined the session_id and session_secret will be used to check that the user is allowed.

The attached code is my attempt to prevent tampering of that cookie. It is cut back so that only the relevant bits are presented. I am fairly sure that it is secure but I cannot be sure.

Please could you look at this code and see if there are any holes. Thank you.

#!/usr/bin/perl use strict; use warnings; use String::CRC::Cksum qw(cksum); use Crypt::CBC; # Utility function. sub display { my ( $first, $second ) = @_; print ' 'x( 16 - length $first ), "$first: '$second'\n"; } my $database_id = 1234; my $session_id = 5678; my $session_secret = join '', 'a'..'z'; # Create the input. my $in = join ':', $database_id, $session_id, $session_secret; display "Input", $in; my $cipher_text = my_encrypt( $in ); display "Encrypted token", $cipher_text; # $cipher_text =~ s/a/b/g; # <= uncomment to muck up the encryption. # display "Altered token", $cipher_text; my $out = my_decrypt( $cipher_text ); display "Output", $out; display 'Result', $in eq $out ? "Good" : "Bad"; ############################################## sub my_encrypt { my $input = shift || die; # Calculate the checksum and then prepend it to the token. my $cksum = cksum( $input ); my $token = join ':', $cksum, $input; display "Token", $token; # Encrypt the token to hex. my $cipher = get_cipher(); my $hex_token = $cipher->encrypt_hex( $token ); return $hex_token; } sub my_decrypt { my $input = shift || die; my $cipher = get_cipher(); my $cksum_token = $cipher->decrypt_hex( $input ); display "Decrypted token", $cksum_token; unless ( $cksum_token ) { warn "Error decrypting"; return 0; } # Extract the checksum and then check it. my ( $cksum, $token ) = split /:/, $cksum_token, 2; # If the checksum is correct. return $token if defined $cksum && $token && $cksum =~ m/^\d+$/ && cksum( $token ) == $cksum; # If it is not correct then complain. warn "Checksum is wrong."; return 0; } sub get_cipher { my $system_secret = 'top secret'; return Crypt::CBC->new ( {'key' => $system_secret, 'cipher' => 'Blowfish' } ) || die; }
I know that there are modules such as Apache::Cookie::Encrypted but I want to be able to put the encrypted token elsewhere and so want a more flexible home grown solution.

--tidiness is the memory loss of environmental mnemonics

Replies are listed 'Best First'.
•Re: Is this a secure way to prevent cookie tampering
by merlyn (Sage) on Jun 29, 2004 at 14:36 UTC
    Stop putting data inside a cookie. Use a cookie to brand a browser, and then use the branding to key into a server-side database. Given that DBM::Deep is pure Perl, even hosted solutions can access decent structured Perl data these days.

    As long as your branding ID is chosen as cryptographically strong, the only thing you have to worry about is a sniffed hijacked session. If you're worried about that, simply use https protocol.

    -- Randal L. Schwartz, Perl hacker
    Be sure to read my standard disclaimer if this is a reply.

      I am using the session_id to brand the browser, with the session_secret to prevent attackers from guessing a valid session_id. The reason behind all the anti-spoof stuff is to allow the initial checks on the cookie to be confident that the client database_id is actually correct. This allows the script to try to connect to the client's database directly instead of having to have to refer to a lookup database on each request.

      Once the client database has been connected to the session details are validated as well.

      --tidiness is the memory loss of environmental mnemonics

        The "session_secret" isn't buying you a thing. Think about the attack vectors. If someone has the session_id, they also have the session_secret, since they got it by sniffing. If someone can guess your session_id, you didn't use a strong enough ID. Just put more bits into one value: no need to separate it into two values.

        Simplify your life. Just use a session_id. That's enough.

        -- Randal L. Schwartz, Perl hacker
        Be sure to read my standard disclaimer if this is a reply.

        Thats the point, stash the sensitive information on the server side and use the session ID as they key to retrieve it. The actions should look like this: User connects to application, app auths user, on successful auth app gets a list of all access levels the user has permission to use. The info is cached into the session. only the session ID is sent back to the user's web browser. on future requests from the user, your app takes the Session ID from the cookie and retrieves the auth/access info from the local (server side) session store (keyed off the session ID). get it now?


        -Waswas
        I, personally, prefer a hybrid solution. First, guessing of keys will always be possible for a user who can ascertain their length. The possibility of them guessing correctly is low, but as you begin to use up possible keys it will inrease. In solutions I use, I tend to have honeypot sessions, unused, that if accessed ban a user temporarily. That takes care of random guessers who try to bruteforce.

        I also log at a basic level users attempts to login (by ip). It's my responsibility as an administrator to decide if a user is attempting to hack the site, or is having genuine problems.

        Furthermore, I _do_ use two session IDs. One of those session IDs is only used securely, though, by setting the cookie to encrypted. That session ID is used for higher level authentication procedures.

        Even moreso, for anything that involves money, or purchases I require users to input their passwords if their session wasn't created recently.

        That keeps sessions pretty damn secure.
Tamper-proofing vs. encryption
by gaal (Parson) on Jun 29, 2004 at 14:31 UTC

    Apart from protecting yourself from spoofing, do you need to encrypt the data inside the cookie?

    If you're just looking for a way to make a tamper-proof ticket, you can send the ticket data in the clear, plus a MAC (message authentication code). One advantage is that if you update the structure of the ticket, you don't need to change the decryption routine: there *is* no decryption routine. You receive a ticket, check that it is valid, and trust everything in it. (Actually, you could refactor your code to allow this in your approach as well.)

    Also, in a real-world case you'll probably want to add an issue timestamp so that you can expire old tickets quickly — in the cleartext version, even before you waste CPU on crypto.

      Thank you. I will look into just using Digest::EMAC instead of all the encryption. I suppose the encryption was just used to ensure that the checksum was not tampered with, which the MAC would achieve.

      --tidiness is the memory loss of environmental mnemonics

Re: Is this a secure way to prevent cookie tampering
by hardburn (Abbot) on Jun 29, 2004 at 14:43 UTC

    No.

    A cookie should store only a randomly-generated unique ID. You store that unique ID in a database somewhere, and everything else about the session is stored there.

    ----
    send money to your kernel via the boot loader.. This and more wisdom available from Markov Hardburn.

      A cookie should store only a randomly-generated unique ID

      Are you suggesting that I should rely on the randomness of the ID to prevent spoofing. Surely storing a serial ID and also a secret unique to the session would be better.

      There are also issues with your approach such as the complexity of checking that the ID is unique and also generating unique numbers when the available pool is largely used. Admittedly these would not be issues for low traffic but they do exist.

      --tidiness is the memory loss of environmental mnemonics

        Are you suggesting that I should rely on the randomness of the ID to prevent spoofing.

        Yes. If it is truely random, you don't have to worry about it. It won't stop replay attacks, but nothing besides strong encryption of the entire session will (such as HTTPS).

        . . . complexity of checking that the ID is unique . . .

        This is as easy as telling your database that the session ID field must be unique. A decent database will do the rest for you.

        . . . generating unique numbers . . .

        Random number generation is a hard problem, but not unsolveable. You benifit from the fact that a lot of people have already studied the problem and implemented solutions. My prefered way is to get some ammount of data from a cryptographically-secure random number generator (like /dev/random) and then put it through a hash (SHA1 prefered; pretend MD5 doesn't exist). SHA1 will then give you a 160-bit value which you store in your database and the user's cookie. That gives you 2**80 possibilities before you can expect keys to collide (due to the Birthday Problem). If you get one visitor per second, you can expect this to happen sometime around the time when the universe can't hold itself together anymore because all the usable energy has been converted to heat. (Of course, this assumes a good RNG).

        ----
        send money to your kernel via the boot loader.. This and more wisdom available from Markov Hardburn.

Re: Is this a secure way to prevent cookie tampering
by Anonymous Monk on Jun 29, 2004 at 16:43 UTC
    Several people have pointed out that it's better to store an opaque id in the cookie, and they are right. However, the cryptography is also somewhat interesting. Your approach is vulnerable to a bit-flipping attack, which lets someone make certain modifications to the cookie even if they can't decrypt it. This is an inherent problem with CBC mode, and Crypt::CBC makes it easily exploitable.
    use Crypt::CBC; my $cbc = Crypt::CBC->new("Blowfish"); my $msg = $cbc->encrypt("foo"); print $cbc->decrypt($msg), "\n"; my $msg2 = $msg ^ (("\0" x 8) . "\4"); print $cbc->decrypt($msg2), "\n"; __OUTPUT__ foo boo
    Notice that I XORed the IV embedded in the ciphertext with 4, and that resulted in the decrypted plaintext being XORed with 4 as well. Combine this with Tweaking CRCs and you can undetectably alter the first few bytes of the cookie.

    I second the recommendation for proper MACs like Digest::EMAC or Digest::HMAC. You can easily get bitten if you try to cook up your own ad-hoc scheme with CRC.

Re: Is this a secure way to prevent cookie tampering
by Jeppe (Monk) on Jun 30, 2004 at 11:21 UTC
    I go with the rest - only store a sufficiently large and random number in the cookie, and store the rest on your server. (That also adds the ability to update the data structures in the cookies).

    However - if you're really concerned about security, remember to use https. That will prevent whoever's eavesdropping from catching your cookie! (This has become much easier since WLAN became popular and "easy-to-use")

Re: Is this a secure way to prevent cookie tampering
by exussum0 (Vicar) on Jun 30, 2004 at 13:02 UTC
    If you need to store any information in the cookie, because doing db lookups for same tiny pieces of information, such as a username, user id or some sorta preference.

    1. Encrypt it.
    2. bin2hex/urlencode/uuencode it.
    3. take the md5 and append it.

    If the md5 of the encrypted info doesn't match the md5 of the encrypted/md5 part, you know someone was tampering with it.

    Yes, you can do a lot of caching tricks server side, but sometimes, you don't have a choice. -s

    Bart: God, Schmod. I want my monkey-man.

      1. Encrypt it.
      2. bin2hex/urlencode/uuencode it.
      3. take the md5 and append it.
      Take the md5 of what? If you md5 the ciphertext, an attacker can still flip bits as I described elsewhere in this thread, then recompute the md5. If you md5 the plaintext, that may allow him to launch a dictionary attack. For best results, use a real keyed MAC function.

      And I thought everybody knew better than to use md5 for new development by now.

        md5 the plain text and then encrypt that. if you are worried about plaintext attacks, gzip the plain text to turn it to binary first.

        TIMTOWTDI.

        Bart: God, Schmod. I want my monkey-man.

Re: Is this a secure way to prevent cookie tampering
by jayrom (Pilgrim) on Jun 30, 2004 at 14:23 UTC
    I agree with hardburn.
    What I use is taken from Lincoln Stein's Apache Modules book. I create a string which is passed to the cookie and is also saved in a database for comparison.
    It might be overkill but the cookie string is updated on each page as one of the fields used to create it is a timestamp. As Lincoln points out this is extremely sensitive to the smallest change in passed parameters, therefore is very hard to spoof and (almost) insures randomness.
    use MD5; my $MAC = MD5->hexhash( $secret. MD5->hexhash(join '', $secret, @fields) );
    The $secret variable holds a 128 character string, which should be as random as possible.
    The @fields array holds whatever data you want to use, as stated before preferably not relevant user data, which combination should of course be unique for each session.
    Changing the $secret string on a regular basis will also provide peace of mind ;-)

    Update

    use MD5; my $MAC = MD5->hexhash( $secret. MD5->hexhash(join ':', $secret, @fields) );
    It looks like the book had a typo as this correct version of the code appears somewhere else in the book.
    Sorry Lincoln, my bad!

    jayrom

      This is almost the same as Digest::HMAC, except that it uses the same $secret for each hash computation.

      The choice of an empty string in the join is not good, though. Do you really want to produce the same authenticator for these two inputs?

      @fields1 = ( "foobar", "baz" ); @fields2 = ( "foo", "barbaz" );
        Very good point!
        Proves that you should never trust code even from the accepted gurus.
        Shame on me ;-)

        jayrom

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://370480]
Approved by sweetblood
Front-paged by grinder
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others romping around the Monastery: (8)
As of 2024-04-23 17:54 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found