SEC.INJ.RAW_SQL_WITHOUT_REASON — Raw SQL that nobody wrote a reason for
- Category: security
- Severity: low
- Level: 0
- Downtime class: none — the finding is about a call site in PHP, not about a statement
- Stability: stable
- Suites:
analyse - Applies to: every engine — it reads PHP source, not a server
What it reports
A call to DB::select(), DB::statement(), DB::unprepared(), DB::insert(), DB::update() or
DB::delete() where nothing at that call site says why raw SQL was chosen.
// reported — the escape hatch was taken and nobody wrote down why
final class OrderReport
{
public function topLineItem(): array
{
return DB::select('SELECT o.id, l.sku FROM orders o, LATERAL (SELECT sku FROM order_lines WHERE order_id = o.id ORDER BY total DESC LIMIT 1) l');
}
}
How to answer it
use Pushery\SQLens\Attributes\RawSql;
final class OrderReport
{
#[RawSql(reason: 'the query builder cannot express a LATERAL join')]
public function topLineItem(): array
{
return DB::select('SELECT o.id, l.sku FROM orders o, LATERAL (SELECT sku FROM order_lines WHERE order_id = o.id ORDER BY total DESC LIMIT 1) l');
}
}
The reason is mandatory, and an empty or whitespace-only one does not count. Nothing parses it: it is written for the next reader, not for this rule.
The attribute targets a method or a class. Prefer the method. A class-level reason covers every call in that class, so a class with one reasoned raw statement and one careless one excuses both — and the careless one silently.
Why Low, when the category is security
Because the rule does not claim your SQL is unsafe. It never reads the query text.
Deciding "is this query dangerous" from a call site is taint analysis, and SQLens does not do taint analysis anywhere. What this rule measures is narrower and much cheaper to be sure about: whether a human stated their reasoning. That is a policy gap.
Rating it High would put it beside findings that describe an actual exposure, and the first team to meet both in one report would learn to discount the severity axis rather than the rule. The injection rules that do look at how a statement was assembled carry their own, higher severities.
The bet behind a justification requirement is small and well-tested in practice: raw SQL is sometimes genuinely the right tool, and a project that has to say why out loud writes less of it by accident.
A reason is not a suppression
| What you are saying | What the report shows | |
|---|---|---|
#[RawSql(reason: '…')] | this is deliberate, and here is why | no finding — the question is answered |
#[SqlensIgnore(rules: […], reason: '…')] | I know, do not tell me | the finding exists, recorded as suppressed |
Do not use #[SqlensIgnore] to answer this rule. It will not silence it, and that is on purpose:
route both through one channel and a report can no longer tell somebody looked at this and accepted
it from this was never a finding.
What it does not claim
- It never reads the query text, so it says nothing about whether values are bound or where they came from. SQLens performs no taint analysis at any point.
- Syntactic detection only. A call reached through a variable, a callable string or a container binding is invisible to it. It reports what it saw and never implies the rest is clean.
- A class-level annotation is coarse, as above.
- A suppression whose rule ids are not literal strings does not suppress. Resolving them would mean evaluating your code during analysis, so the finding stays visible — the safe direction.
Turning it on
It ships in the analyse suite's PHPStan extension:
includes:
- vendor/pushery/sqlens-for-laravel/extension.neon
See the analyse suite for how it sits beside
Larastan and phpstan-dba.