From 1a99afb9c8420be3d075e6b32fad8984a362dd69 Mon Sep 17 00:00:00 2001 From: Nicolo Singer Date: Mon, 22 Sep 2025 11:21:04 -0500 Subject: [PATCH] fix(formatter): prevent stored XSS by disabling default HTML safety Formatters no longer treat their output as HTML safe by default. This closes a stored XSS vector where unsanitized user input could inject script content. Existing formatters must now explicitly implement isHtmlSafe() to return true *and* ensure proper escaping/sanitization before claiming safety. BREAKING CHANGE: Default formatter behavior changed; outputs are now considered unsafe HTML unless explicitly marked safe. Audit custom formatter implementations. Signed-off-by: Nicolo Singer --- .github/workflows/ci.yaml | 2 +- Makefile | 2 +- composer.json | 7 ++++++- phpstan.neon | 2 -- src/Resources/views/tailwind_2/_table.html.twig | 6 ++++-- src/Twig/TableRenderExtension.php | 6 ++++-- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0f1c83d..862a3e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,7 +49,7 @@ jobs: SYMFONY_REQUIRE: ${{ matrix.symfony }} uses: ramsey/composer-install@v2 - name: Run test suite on PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} - run: vendor/bin/simple-phpunit + run: vendor/bin/phpunit - name: Run ECS run: vendor/bin/ecs - name: Run PHPStan diff --git a/Makefile b/Makefile index 68e280d..0ad0835 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,6 @@ styles: ## PHP Unit phpunit: - vendor/bin/simple-phpunit + vendor/bin/phpunit diff --git a/composer.json b/composer.json index 15ea69f..dc44ac7 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,8 @@ "symfony/stimulus-bundle": "^2.16" }, "require-dev": { + "araise/core-bundle": "dev-develop as 1.1", + "araise/search-bundle": "dev-develop as 3.1", "symfony/phpunit-bridge": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", @@ -40,7 +42,10 @@ "symfony/webpack-encore-bundle": "^1.14|^2.1", "symfony/security-core": "^6.4|^7.0", "symfony/security-bundle": "^6.4|^7.0", - "phpstan/phpstan": "^1.5" + "phpstan/phpstan": "^1.5", + "slevomat/coding-standard": "8.22.1", + "phpunit/phpunit": "^10", + "symfony/test-pack": "^1.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon index f390efa..31f6166 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,5 +3,3 @@ parameters: paths: - src - tests - bootstrapFiles: - - vendor/bin/.phpunit/phpunit-9.6-0/vendor/autoload.php diff --git a/src/Resources/views/tailwind_2/_table.html.twig b/src/Resources/views/tailwind_2/_table.html.twig index 4717c37..46b5f87 100644 --- a/src/Resources/views/tailwind_2/_table.html.twig +++ b/src/Resources/views/tailwind_2/_table.html.twig @@ -221,7 +221,8 @@ {% endif %} {% endif %} {% endapply %} - {{ araise_table_column_render(column, row) }} + {% set columnValue = araise_table_column_render(column, row) %} + {{ wwd_is_html_safe(column) ? columnValue|raw : columnValue }} {% if column.option('link_the_column_content') and columnLink|default(false) %}{% endif %} {% endfor %} @@ -312,7 +313,8 @@ class="px-3 py-2 {{ footerColumn.option('attributes')['class'] ?? '' }}" {{ attr|map((value, attr) => "#{attr}=\"#{value}\"")|join(' ')|raw }} > - {{ araise_table_column_render(footerColumn, table.footerData) }} + {% set columnValue = araise_table_column_render(footerColumn, table.footerData) %} + {{ wwd_is_html_safe(footerColumn) ? columnValue|raw : columnValue }} {% endfor %} diff --git a/src/Twig/TableRenderExtension.php b/src/Twig/TableRenderExtension.php index 8a7682e..9164af0 100644 --- a/src/Twig/TableRenderExtension.php +++ b/src/Twig/TableRenderExtension.php @@ -27,18 +27,20 @@ public function __construct( public function getFunctions(): array { - $options = [ + + $options = $noSafeOptions = [ 'needs_context' => true, 'is_safe' => ['html'], 'is_safe_callback' => true, 'blockName' => 'blockName', ]; + $noSafeOptions['is_safe'] = []; return [ new TwigFunction('araise_table_render', fn ($context, Table $table) => $this->renderTable($context, $table), $options), new TwigFunction('araise_table_only_render', fn ($context, Table $table) => $this->renderTable($context, $table, 'table_table'), $options), new TwigFunction('araise_table_action_render', fn ($context, Action $action, $entity) => $this->renderTableAction($context, $action, $entity), $options), - new TwigFunction('araise_table_column_render', fn ($context, Column $column, $entity) => $this->renderTableColumn($context, $column, $entity), $options), + new TwigFunction('araise_table_column_render', fn ($context, Column $column, $entity) => $this->renderTableColumn($context, $column, $entity), $noSafeOptions), ]; }