From bde10bf1ea693979b44a701dd7f4a69aa5be31f4 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 28 Dec 2025 20:12:43 +0900 Subject: [PATCH] Add Psalm taint annotations for SQL injection analysis Add @psalm-taint-escape sql annotations to indicate that values passed to SQL query methods are safely escaped via prepared statements. Annotated classes: - PerformSqlInterface::perform() - interface declaration - PerformSql::perform() - prepared statement execution - PerformTemplatedSql::perform() - templated SQL execution - SqlQueryInterface - all query methods (getRow, getRowList, exec, getCount, getPages) - SqlQuery - implementation of all query methods These annotations enable Psalm's taint analysis to understand that user input flowing through these methods is properly escaped when used with prepared statements. --- src/PerformSql.php | 1 + src/PerformSqlInterface.php | 2 ++ src/PerformTemplatedSql.php | 1 + src/SqlQuery.php | 10 ++++++++++ src/SqlQueryInterface.php | 18 ++++++++++++++++-- tests/sql/create_promise.sql | 7 ++++--- tests/sql/create_todo.sql | 5 ++--- 7 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/PerformSql.php b/src/PerformSql.php index 9d374975..1c050199 100644 --- a/src/PerformSql.php +++ b/src/PerformSql.php @@ -10,6 +10,7 @@ final class PerformSql implements PerformSqlInterface { + /** @psalm-taint-escape sql */ #[Override] public function perform(ExtendedPdoInterface $pdo, string $sqlId, string $sql, array $values): PDOStatement { diff --git a/src/PerformSqlInterface.php b/src/PerformSqlInterface.php index d4ac02f5..33915240 100644 --- a/src/PerformSqlInterface.php +++ b/src/PerformSqlInterface.php @@ -18,6 +18,8 @@ interface PerformSqlInterface * @param array $values The values to bind to the SQL statement. * * @return PDOStatement The result of the performed SQL statement. + * + * @psalm-taint-escape sql */ public function perform(ExtendedPdoInterface $pdo, string $sqlId, string $sql, array $values): PDOStatement; } diff --git a/src/PerformTemplatedSql.php b/src/PerformTemplatedSql.php index b4892dc0..bfbba110 100644 --- a/src/PerformTemplatedSql.php +++ b/src/PerformTemplatedSql.php @@ -19,6 +19,7 @@ public function __construct( ) { } + /** @psalm-taint-escape sql */ #[Override] public function perform(ExtendedPdoInterface $pdo, string $sqlId, string $sql, array $values): PDOStatement { diff --git a/src/SqlQuery.php b/src/SqlQuery.php index c267110f..77b1cd20 100644 --- a/src/SqlQuery.php +++ b/src/SqlQuery.php @@ -54,6 +54,8 @@ public function __construct( /** * {@inheritDoc} + * + * @psalm-taint-escape sql */ #[Override] public function exec(string $sqlId, array $values = [], FetchInterface|null $fetch = null): void @@ -63,6 +65,8 @@ public function exec(string $sqlId, array $values = [], FetchInterface|null $fet /** * {@inheritDoc} + * + * @psalm-taint-escape sql */ #[Override] public function getRow(string $sqlId, array $values = [], FetchInterface|null $fetch = null): array|object|null @@ -80,6 +84,8 @@ public function getRow(string $sqlId, array $values = [], FetchInterface|null $f /** * {@inheritDoc} + * + * @psalm-taint-escape sql */ #[Override] public function getRowList(string $sqlId, array $values = [], FetchInterface|null $fetch = null): array @@ -92,6 +98,8 @@ public function getRowList(string $sqlId, array $values = [], FetchInterface|nul /** * {@inheritDoc} + * + * @psalm-taint-escape sql */ #[Override] public function getCount(string $sqlId, array $values): int @@ -193,6 +201,8 @@ public function getStatement(): PDOStatement /** * {@inheritDoc} + * + * @psalm-taint-escape sql */ #[Override] public function getPages(string $sqlId, array $values, int $perPage, string $queryTemplate = '/{?page}', string|null $entity = null): PagesInterface diff --git a/src/SqlQueryInterface.php b/src/SqlQueryInterface.php index f8767e83..a781d199 100644 --- a/src/SqlQueryInterface.php +++ b/src/SqlQueryInterface.php @@ -10,6 +10,8 @@ interface SqlQueryInterface * @param array $values * * @return array|object|null + * + * @psalm-taint-escape sql */ public function getRow(string $sqlId, array $values = [], FetchInterface|null $fetch = null): array|object|null; @@ -17,18 +19,30 @@ public function getRow(string $sqlId, array $values = [], FetchInterface|null $f * @param array $values * * @return array> + * + * @psalm-taint-escape sql */ public function getRowList(string $sqlId, array $values = [], FetchInterface|null $fetch = null): array; - /** @param array $values */ + /** + * @param array $values + * + * @psalm-taint-escape sql + */ public function exec(string $sqlId, array $values = [], FetchInterface|null $fetch = null): void; - /** @param array $values */ + /** + * @param array $values + * + * @psalm-taint-escape sql + */ public function getCount(string $sqlId, array $values): int; /** * @param array $values * @param ?class-string $entity + * + * @psalm-taint-escape sql */ public function getPages(string $sqlId, array $values, int $perPage, string $queryTemplate = '/{?page}', string|null $entity = null): PagesInterface; } diff --git a/tests/sql/create_promise.sql b/tests/sql/create_promise.sql index 11507467..ac94e44c 100644 --- a/tests/sql/create_promise.sql +++ b/tests/sql/create_promise.sql @@ -1,5 +1,6 @@ -CREATE TABLE IF NOT EXISTS todo +CREATE TABLE IF NOT EXISTS promise ( - id INTEGER, - title TEXT + id TEXT, + title TEXT, + time TEXT ) diff --git a/tests/sql/create_todo.sql b/tests/sql/create_todo.sql index ac94e44c..a6e19e63 100644 --- a/tests/sql/create_todo.sql +++ b/tests/sql/create_todo.sql @@ -1,6 +1,5 @@ -CREATE TABLE IF NOT EXISTS promise +CREATE TABLE IF NOT EXISTS todo ( id TEXT, - title TEXT, - time TEXT + title TEXT )