From 5db858a8afd2b6e7cefcafb3bea309bd0ad9f53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Franco?= Date: Wed, 5 Aug 2026 12:28:09 +0200 Subject: [PATCH 1/2] fix(security): stop marking Twig serial helpers as HTML-safe --- docs/SECURITY.md | 11 ++++------- src/Twig/SerialNumberTwigExtension.php | 2 -- tests/Unit/Twig/SerialNumberTwigExtensionTest.php | 3 +++ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 9dd589d..81947d1 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -47,14 +47,11 @@ Mitigations address **resource exhaustion (DoS)** and document **XSS** considera ## 2. XSS and user-controlled data -Twig functions/filters use **`is_safe => ['html']`**, so Twig does not escape the output. That is appropriate when values are **application-controlled** (e.g. system-generated invoice numbers). +Twig function `serial_number` and filter `serial_number_mask` do **not** mark output as HTML-safe (`is_safe` is not set). Twig **auto-escapes** their results in HTML templates by default. -- **Recommendation:** Do not pass unsanitized user input (forms, query strings, etc.) directly into `serial_number()` or `serial_number_mask()` without validating/escaping the result for HTML. -- If serials or context values may contain user content: - - Escape in the template (e.g. `{{ serial|serial_number_mask(4)|e }}` when you need escaping for that value), or - - Ensure values are sanitized before they reach the bundle. - -The bundle does not HTML-escape; the application must use serials safely in HTML context. +- Prefer application-controlled values (e.g. system-generated invoice numbers). +- If serials or context values may contain user content, keep auto-escaping enabled; do not pipe through `|raw` unless the value is already sanitized for HTML. +- Outside HTML contexts (e.g. attributes, JS), escape appropriately for that context. --- diff --git a/src/Twig/SerialNumberTwigExtension.php b/src/Twig/SerialNumberTwigExtension.php index 51fff4e..c30988f 100644 --- a/src/Twig/SerialNumberTwigExtension.php +++ b/src/Twig/SerialNumberTwigExtension.php @@ -46,7 +46,6 @@ public function getFunctions(): array new TwigFunction( 'serial_number', $this->generateSerialNumber(...), - ['is_safe' => ['html']], ), ]; } @@ -62,7 +61,6 @@ public function getFilters(): array new TwigFilter( 'serial_number_mask', $this->maskSerialNumber(...), - ['is_safe' => ['html']], ), ]; } diff --git a/tests/Unit/Twig/SerialNumberTwigExtensionTest.php b/tests/Unit/Twig/SerialNumberTwigExtensionTest.php index 1a9eb79..64ad871 100644 --- a/tests/Unit/Twig/SerialNumberTwigExtensionTest.php +++ b/tests/Unit/Twig/SerialNumberTwigExtensionTest.php @@ -7,6 +7,7 @@ use Nowo\SerialNumberBundle\Service\SerialNumberGenerator; use Nowo\SerialNumberBundle\Twig\SerialNumberTwigExtension; use PHPUnit\Framework\TestCase; +use Twig\Node\Node; use function strlen; @@ -72,6 +73,7 @@ public function testGetFunctionsReturnsSerialNumber(): void $functions = $this->extension->getFunctions(); self::assertCount(1, $functions); self::assertSame('serial_number', $functions[0]->getName()); + self::assertNull($functions[0]->getSafe(new Node())); } public function testGetFiltersReturnsSerialNumberMask(): void @@ -79,6 +81,7 @@ public function testGetFiltersReturnsSerialNumberMask(): void $filters = $this->extension->getFilters(); self::assertCount(1, $filters); self::assertSame('serial_number_mask', $filters[0]->getName()); + self::assertNull($filters[0]->getSafe(new Node())); } public function testMaskSerialNumberNegativeVisibleLastTreatedAsZero(): void From 1f346c80c6b463b8c789a16b8315df4d6fe18098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Franco?= Date: Wed, 5 Aug 2026 12:39:26 +0200 Subject: [PATCH 2/2] test(security): assert Twig getSafe returns empty array without is_safe --- demo/symfony8/config/reference.php | 2 ++ tests/Unit/Twig/SerialNumberTwigExtensionTest.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/demo/symfony8/config/reference.php b/demo/symfony8/config/reference.php index 61cff2e..bdf34f4 100644 --- a/demo/symfony8/config/reference.php +++ b/demo/symfony8/config/reference.php @@ -1,5 +1,7 @@ extension->getFunctions(); self::assertCount(1, $functions); self::assertSame('serial_number', $functions[0]->getName()); - self::assertNull($functions[0]->getSafe(new Node())); + self::assertSame([], $functions[0]->getSafe(new Node())); } public function testGetFiltersReturnsSerialNumberMask(): void @@ -81,7 +81,7 @@ public function testGetFiltersReturnsSerialNumberMask(): void $filters = $this->extension->getFilters(); self::assertCount(1, $filters); self::assertSame('serial_number_mask', $filters[0]->getName()); - self::assertNull($filters[0]->getSafe(new Node())); + self::assertSame([], $filters[0]->getSafe(new Node())); } public function testMaskSerialNumberNegativeVisibleLastTreatedAsZero(): void