Do everyone a favor. Don't use PHP.
It's a list of lists.
- https://en.wikiquote.org/wiki/Rasmus_Lerdorf (creator)
My own horror story
People write code. I get paid to get that code running in production. Easy job right? As long as you don't have to run Symfony.
The interwebs is not a safe place
Now if you know a thing or two about running any sort of network services, you're probably aware that the Internet is full of danger for such little apps. Every Internet host is a yummy snack for the bad guys, either because you can send spam, mine bitcoins, or use it in further attacks. You don't want this, because this will interfere with legit services that your box is trying to provide. Yuck.
So you apply the simplest and most important principle in computer security: privilege separation. You can minimize the amount of damage that a compromised application can deal to your system.
The simplest possible step you can take, is to forbid the application any write rights to the filesystem, except for perhaps
/tmp or some user uploads area. You should also make sure, that even if the application can write in such places, it won't be able to execute any code that resides there. We can say that we're trying to enforce a principle called
W^X, or execution prevention.
PHP is well known for making each and every file with the
.php extension a possible entry point to your application. This supposedly simplifies deployments (just copy files using the FTP Sadness to the target machine!), but it also creates up a wide range of security-related headaches.
Think: many web applications will allow uploads of user-generated data to some sort of on-disk directory, where these files are later made available by the same web server daemon that allowed the upload in the first place. Can you connect the dots?
Smart developers put all but one PHP files outside of the web server's document root, and use that single "proxy" file to load the rest of the application. Smart sysadmins teach the web server to deny inclusion or execution of PHP files outside of specific, whitelisted paths. So far, so good.
PHP's execution model is extremely performance-unfriendly: the entire application's source code is parsed and recompiled on each and every request. Various methods have been developed to mitigate this problem. Symfony has one as well: the (dreaded) cache.
So what Symfony does: Symfony has a "cache". This cache mechanism, it reads, parses, "transpiles" (is that a word?) and packages the entire application source (plus its runtime configuration) and writes a bunch of "optimized" PHP code; not unlike what JS compilers/minifiers do. The performance benefits during runtime are obviously tremendous.
However, Symfony has a particular peculiarity regarding this cache. It insists that the cache be generated upon the first request that hits the application. (See the problem yet?)
In order for this to happen, the application demands that an area be both writable by itself and PHP-executable, even if the associated tooling allows to purge and generate the cache in an offline mode.
No, seriously. Go read what the official Symfony documentation advises on this matter. I'll wait here for you, and when you're back, I'll iterate on how sad this makes me.
- Use the same user for the CLI and the web server
The cache files must be writable by the user that executes the code, and this is the simplest way to get this done. The documentation authors were kind enough to point out that this is an actual security concern.
- Using ACL on a system that supports chmod +a (MacOS X)
- Using ACL on a system that supports setfacl (most Linux/BSD)
First off, ACL's are non-portable, require special filesystem features, and add unnecessary complexity to file permission management.
And this doesn't fix a damn thing. Your application can still write and execute.
- Without using ACL
The guide suggests setting an
000. This makes the created files not just writable by the same user, but also the user's primary group, or even the world.
It makes the problem objectively worse.
None of these are fixing the root problem. Yet the solution is so simple: do not try writing to the damn cache. Leave it up to the system administrator to generate it, and run without it, or with the old version, for the 2 seconds it takes to run the command.
We ended up using option 1., as it was the simplest way forward and posed the smallest security risk.
Symfony depends on, and heavily integrates Composer, PHP's answer to pip, npm, gems, etc.
What happens when you need to update library dependencies between app code updates?
Composer tries to (yes, you guessed it) clear Symfony's cache.
Obviously, in this case, it is impossible to run it as a separate user, so that the installed dependencies don't end up writable by the application.
chmod -Rcan be applied next, but this adds an extra deployment step and still leaves the system vulnerable during a deployment.
Wiping the cache prior to running Composer doesn't help, because Symfony will regenerate it as soon as the first request hits. This makes the procedure practically impossible to carry out on a busy host.
One idea that our team has had, was to put up a simple, static page informing that maintenance is in progress. This obviously sucks.
Another idea is to have several hosts behind a load balancer, and update the code in batches, taking each host off the LB pool for the update. (Naturally it's impossible without at least two hosts and a load balancer, but if you don't have a redundant setup, you probably don't care about uptime in the first place!)
Yet another idea (we have a working PoC) is to hack Composer, to stop it from requiring this sadness. Keeping hacked forks around is not really a good solution though.
So much for simple deployments.
I wanted to write about Wordpress, but decided not to.