views:

392

answers:

9

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>
A: 

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.

gbrandt
A: 

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.

zodeus
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
Have each window create a new index in the session. Or create a file for each object on the server
zodeus
A: 

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
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
A: 

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
+1  A: 

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
That is a good answer. I would then have to periodically clean the uncreated invoices table (people may drop the session), and also build my objects from the table at every page load. +1
Martin
Oh, i totally forgot about that! Thanks!
Richard Levasseur
+2  A: 

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
I like this method, but it suffers from the same problems as sessions have where multiple windows control the same cookie.
Martin
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
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
That sounds reasonable to me, if not more complex than my current method. +1
Martin
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
You can also certainly just forgo the cookies and store the base64 encoded data in a hidden form field.
RibaldEddie
+3  A: 

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
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
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
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
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
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
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
A: 

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

Karsten
+1  A: 

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
Yes, I could do that. But my current method is idempotent. +1
Martin