1

I have a PHP application where I would like to certain objects to persist in the following manner:

  1. The object must not exist in the $_SESSION. Separate web browser windows must control separate instances of the object.
  2. The end-user must not be able to modify the object by changing the content of the $_REQUEST variable by hand (if this happens the request should be treated as corrupted).

Is there a best-practices / proper way to do this? With PHP becoming more and more object oriented, I fear that I am reinventing a wheel.

The grand purpose of this code is to allow the creation and manipulation of complex objects without using a database until they are to be committed, then I will use a proper transaction to commit them to the database in full. I want to make it so that my database contains only the complete invoice, or no invoice at all.

My current method is as follows:

<?php

include('encrypt.php');
include('invoice.class.php');

if(isset($_REQUEST['invoice']))
{
    $invoice = unserialize(decrypt(base64_decode($_REQUEST['invoice'])));
    if(!($invoice instanceOf invoice)) throw new exception('Something bad happened');
}
else
{
    // Some pages throw an exception if the $_REQUEST doesn't exist.
    $invoice = new invoice();
}

if(isset($_REQUEST['action']) && $_REQUEST['action'] == 'addLine')
{
    $invoice->addLine(new invoiceLine($_REQUEST['description'], $_REQUEST['qty'], $_REQUEST['unitprice']);
}

?>
<form action="index.php" method="post">
<input type="text" name="qty" />
...
<input type="hidden" name="invoice" value="<?php echo(base64_encode(encrypt(serialize($invoice)))); ?>" />
</form>
Martin
  • 5,945
  • 7
  • 50
  • 77

9 Answers9

4

Here's a trick: put it in a cookie!

Neat algorithm:

$data = serialize($object); $time = time(); $signature = sha1($serverSideSecret . $time . $data); $cookie = base64("$signature-$time-$data");

The benefit is that you

a) can expire the cookie when you want because you are using the timestamp as part of the signature hash.

b) can verify that the data hasn't been modified on the client side, because you can recreate the hash from the data segment in the cookie.

Also, you don't have to store the entire object in the cookie if it will be too big. Just store the data you need on the server and use the data in the cookie as a key.

I can't take credit for the algorithm, I learned it from Cal Henderson, of Flickr fame.

Edit: If you find using cookies too complicated, just forget about them and store the data that would have gone in the cookie in a hidden form field.

RibaldEddie
  • 5,136
  • 2
  • 23
  • 22
  • I like this method, but it suffers from the same problems as sessions have where multiple windows control the same cookie. – Martin Mar 06 '09 at 07:09
  • Cookies are document specific, and you can manage them with javascript. As long as the SHA1 signature is calculated on the server (to keep the hash salt secret), you can do the rest on the client in JS, and make each sure each cookie has a documentId in it. – RibaldEddie Mar 06 '09 at 07:14
  • Just to be clear, you can store multiple cookies with a single domain path. Just make sure that each window gets its own cookie. – RibaldEddie Mar 06 '09 at 07:18
  • That sounds reasonable to me, if not more complex than my current method. +1 – Martin Mar 06 '09 at 07:24
  • I guess it is a tad more complex, but there's something simply elegant about signing the serialized object and making the user store it. – RibaldEddie Mar 06 '09 at 07:30
  • You can also certainly just forgo the cookies and store the base64 encoded data in a hidden form field. – RibaldEddie Mar 06 '09 at 07:34
  • Also remember that certain browsers put a limit on the amount of data passed via headers – buggedcom Apr 08 '11 at 10:27
3

You could also save state on the client, without cookies, using a simple hidden form input. As long as the the data (probably a serialized blob) is encrypted and signed, the user can't modify it without breaking their session.

Steve Gibson uses this method for his custom e-commerce system. While his code isn't open source, he thoroughly explains ways to save state without storing sensitive data on the server or requiring cookie support in Security Now Episode #109, "GRC's eCommerce System".

Bob Somers
  • 7,266
  • 5
  • 42
  • 46
  • Great answer. Page 14 of the Security Now Episode #109 transcript contains the information I wanted. He did the exact same thing as I have done. – Martin Mar 06 '09 at 07:57
  • Before storing encrypted data on the client side, also read: http://stackoverflow.com/questions/606179/what-encryption-algorithm-is-best-for-encrypting-cookies/606201#606201 – Jacco Mar 06 '09 at 14:46
  • Unless I'm mistaken, that post suggests you can brute force AES 256 without much effort, which is a laughable assumption. No force on earth could brute force AES 256 with a well-chosen key and a good salt. Even if the encryption was cracked, the signature prevents tampering. – Bob Somers Mar 06 '09 at 21:37
  • The 'breaking AES 256' is part is overrated, but the main theme of the answers is: do not send out data to the client unless there is no other way. – Jacco Mar 06 '09 at 22:46
  • I still don't necessarily agree with that. If it's encrypted and signed correctly it is as secure as it would be on your server. As a bonus, for e-commerce applications it prevents you from having to store sensitive data (like CC numbers) on your server, so it's easier to CYA. – Bob Somers Mar 06 '09 at 23:19
  • Maybe you are right, if you put CC data in a cookie on the client machine, you spread the risk. But you also lose the ability to control the safety of the system that stores the data. Maybe this should be a whole new question. – Jacco Mar 07 '09 at 23:33
2

What I would do is store a cryptographic key (not the whole structure, like your example is) in a hidden form variable. This key is an index to a uncreated_invoices table where you store incomplete invoices.

Between pages, you update the data in the uncreated_invoices table, and when they're done, pull it out and commit it. If you put the username into the uncreate_invoices table, this will also let them pick up where they left off (not sure if thats a valid use case). It'd probably be a good idea to put the username in it anyways so people can't try to hijack other people's invoices.

You can probably get away with storing serialized data in the uncreated_invoices table, if you wanted. Personally, i'd normalize it a bit (how much depends on your schema) so you can easily add/remove individual pieces.

Edit: I forgot to mention that you should clean the uncreated_invoices table periodically so it doesn't fill up with stale invoices.

Richard Levasseur
  • 14,562
  • 6
  • 50
  • 63
1

Couldn't you store the data in a array within $_SESSION, and then have a unique id for each window. If each window has unique id you can pass the id around as part of your forms. Store/retrieve the data in the session or database based on the window id.

So something like this? $_SESSION['data'][$windowid]->$objectname

Zoredache
  • 37,543
  • 7
  • 45
  • 61
0

If you can't use SESSION then you must persist the data yourself. You must place the data somewhere will it will persist, such as a database or some other file. Its the only way, besides SESSION to persit data.

I take that back, you can send the data in each HTML page and use it locally. Every page that accepts the data must create HTML/Javascript that continues the data sequence.

Gregor Brandt
  • 7,659
  • 38
  • 59
0

If you store things on the client side they can be modified. Is there a specific reason you don't want to store the objects in a session? If a storing the object actually isn't viable you'll need to persist it somewhere else on the server.

Hawk Kroeger
  • 2,336
  • 17
  • 21
  • If you have multiple windows open, keeping the object in the session will cause both windows to modify the same object. This is unacceptable to my application. – Martin Mar 06 '09 at 06:18
  • Have each window create a new index in the session. Or create a file for each object on the server – Hawk Kroeger Mar 06 '09 at 06:22
0

That's fine, they'll be able to add all the items they want. The problem in your code is that you're taking the item descriptor, quantity, AND PRICE from the request, the price really should be looked up in the background, otherwise, a user could hand craft their price. Heck, negative value items, and you get stuff for free.

Saem
  • 3,403
  • 23
  • 12
  • The example application is not meant to sell something to the user, but instead to let the user create their own invoices online and keep a record. – Martin Mar 06 '09 at 06:22
0

You want to store something. That's usually done in databases. How do you know the price of something without looking it up in a database? So use the same database to store information about the current user/session.

jmucchiello
  • 18,754
  • 7
  • 41
  • 61
0

You could use the magic methods __sleep() and __wakeup()

Karsten
  • 14,572
  • 5
  • 30
  • 35