Journal — Security
Illustration : auteur inconnu — CC0 1.0 · source
Three patterns come back in almost every audit of a made-to-order module. None of them is exotic, all of them are avoidable, and reading the code is enough to spot them.
A bespoke module flies under the radar. It has no community reporting problems, no security bulletins, no automatic updates. It does its job, year after year, and nobody reads it again.
Here are the three patterns I find most often, in the order I look for them.
By far the most frequent, and it always takes the same shape: an identifier read from the address bar or from a form, dropped as-is into an SQL string.
// don't do this
$sql = 'DELETE FROM '.$prefix.'table WHERE id = "'.$id.'"';
What makes the pattern hard to spot is that it sits next to correct code. In the same file, often in the same method, the insert escapes its values properly while the delete, written three minutes later, does not. This is not a matter of skill: it is a matter of review.
The fix is mechanical. An identifier is cast to an integer, a string goes through the platform's escaping function, and parameters are read from the request object rather than from globals — which also handles the case of a missing parameter.
The second pattern is quieter. The page lives in the back office, so “it is protected”: you have to be logged in to reach it.
Except that being authenticated is not being authorised. An account created for a seasonal worker, an outside contractor or an intern often has a very restricted profile — and yet ends up able to trigger an action touching data that is none of their business, because the module never checked anything beyond the presence of a session.
The question to ask about every sensitive action is not “who can get here”, but “which profile is allowed to do this”, and the answer has to be written in the code.
Catalogue import, image upload, document generation: as soon as a filename comes from outside, it must be treated as hostile. A name can contain what it takes to climb the directory tree, and an extension can lie about the content.
The rule that avoids most of it: never reuse the supplied name. Generate a name, impose the directory, validate the real type of the content, and store it outside the folders served directly by the web server.
Without being a developer, you can already ask three questions of whoever maintains your modules:
Three clear answers are worth more than a hundred-page report. And if the answers are vague, that is precisely the moment to have the code reviewed — before somebody else does it for you.
Migration Symfony jumps from 4.4 to 6.4 and FrameworkBundleAdminController disappears. An inventory of what breaks, module by module, and of what doesn't move.
Takeover No access, no repository, no documentation. The audit method I apply before saying whether to repair or rebuild.
Field report Three Symfony versions, two PHP versions, one codebase. I tried it on my own modules — here is where the line falls between one branch and two.