From 90f0fc13e6ca8aafb9d5ac29643a75c403d10fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Franco?= Date: Wed, 19 Aug 2026 18:29:43 +0200 Subject: [PATCH 1/3] security: enforce url_allowlist when url_allowlist_required --- .../0.1/config/packages/nowo_qr_code.yaml | 1 + src/DependencyInjection/Configuration.php | 4 ++ .../NowoQrCodeExtension.php | 1 + .../UrlAllowlistValidationPass.php | 39 +++++++++++++++ src/NowoQrCodeBundle.php | 2 + .../UrlAllowlistValidationPassTest.php | 50 +++++++++++++++++++ 6 files changed, 97 insertions(+) create mode 100644 src/DependencyInjection/UrlAllowlistValidationPass.php create mode 100644 tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php diff --git a/.symfony/recipe/nowo-tech/qr-code-bundle/0.1/config/packages/nowo_qr_code.yaml b/.symfony/recipe/nowo-tech/qr-code-bundle/0.1/config/packages/nowo_qr_code.yaml index 347b6ad..1226b80 100644 --- a/.symfony/recipe/nowo-tech/qr-code-bundle/0.1/config/packages/nowo_qr_code.yaml +++ b/.symfony/recipe/nowo-tech/qr-code-bundle/0.1/config/packages/nowo_qr_code.yaml @@ -18,6 +18,7 @@ nowo_qr_code: when@prod: nowo_qr_code: + url_allowlist_required: true profiles: default: # Set host allowlist in production when encoding URLs (not plain text). diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 386c130..621ded2 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -51,6 +51,10 @@ public function getConfigTreeBuilder(): TreeBuilder }) ->end() ->children() + ->booleanNode('url_allowlist_required') + ->defaultFalse() + ->info('When true, container compilation fails if the default profile url_allowlist is empty (production hardening).') + ->end() ->booleanNode('use_database_config') ->info('When true, Doctrine rows with the same profile name fully override YAML profiles; enables admin CRUD and requires doctrine/orm') ->defaultFalse() diff --git a/src/DependencyInjection/NowoQrCodeExtension.php b/src/DependencyInjection/NowoQrCodeExtension.php index 793b735..93c0098 100644 --- a/src/DependencyInjection/NowoQrCodeExtension.php +++ b/src/DependencyInjection/NowoQrCodeExtension.php @@ -182,6 +182,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('nowo_qr_code.margin', $default['margin']); $container->setParameter('nowo_qr_code.error_correction', $default['error_correction']); $container->setParameter('nowo_qr_code.url_allowlist', $default['url_allowlist']); + $container->setParameter('nowo_qr_code.url_allowlist_required', $config['url_allowlist_required']); $container->setParameter('nowo_qr_code.use_database_config', $config['use_database_config']); $container->setParameter('nowo_qr_code.doctrine.table_prefix', $config['doctrine']['table_prefix']); $container->setParameter('nowo_qr_code.security.access_roles', $config['security']['access_roles']); diff --git a/src/DependencyInjection/UrlAllowlistValidationPass.php b/src/DependencyInjection/UrlAllowlistValidationPass.php new file mode 100644 index 0000000..0e48063 --- /dev/null +++ b/src/DependencyInjection/UrlAllowlistValidationPass.php @@ -0,0 +1,39 @@ +hasParameter(self::PARAM_ALLOWLIST)) { + return; + } + + $allowlistRequired = $container->hasParameter(self::PARAM_ALLOWLIST_REQUIRED) + && (bool) $container->getParameter(self::PARAM_ALLOWLIST_REQUIRED); + + if (!$allowlistRequired) { + return; + } + + /** @var list $allowlist */ + $allowlist = $container->getParameter(self::PARAM_ALLOWLIST); + + if ($allowlist === []) { + throw new InvalidConfigurationException('nowo_qr_code.url_allowlist_required is true but the default profile url_allowlist is empty. Add host patterns (or set url_allowlist_required: false for local demos only).'); + } + } +} diff --git a/src/NowoQrCodeBundle.php b/src/NowoQrCodeBundle.php index 7f3e7fb..f791fd6 100644 --- a/src/NowoQrCodeBundle.php +++ b/src/NowoQrCodeBundle.php @@ -6,6 +6,7 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; use Nowo\QrCodeBundle\DependencyInjection\Compiler\TwigPathsPass; +use Nowo\QrCodeBundle\DependencyInjection\UrlAllowlistValidationPass; use Nowo\QrCodeBundle\DependencyInjection\NowoQrCodeExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; @@ -23,6 +24,7 @@ public function build(ContainerBuilder $container): void { parent::build($container); $container->addCompilerPass(new TwigPathsPass()); + $container->addCompilerPass(new UrlAllowlistValidationPass()); $entityDir = __DIR__ . '/Entity'; if (class_exists(DoctrineOrmMappingsPass::class) && is_dir($entityDir)) { diff --git a/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php b/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php new file mode 100644 index 0000000..73ffb1a --- /dev/null +++ b/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php @@ -0,0 +1,50 @@ +setParameter('nowo_qr_code.url_allowlist', []); + $container->setParameter('nowo_qr_code.url_allowlist_required', true); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('url_allowlist_required is true'); + + (new UrlAllowlistValidationPass())->process($container); + } + + public function testPassesWhenRequiredAndAllowlistNonEmpty(): void + { + $container = new ContainerBuilder(); + $container->setParameter('nowo_qr_code.url_allowlist', ['example.com']); + $container->setParameter('nowo_qr_code.url_allowlist_required', true); + + (new UrlAllowlistValidationPass())->process($container); + + self::assertTrue(true); + } + + public function testPassesWhenNotRequiredAndAllowlistEmpty(): void + { + $container = new ContainerBuilder(); + $container->setParameter('nowo_qr_code.url_allowlist', []); + $container->setParameter('nowo_qr_code.url_allowlist_required', false); + + (new UrlAllowlistValidationPass())->process($container); + + self::assertTrue(true); + } +} From 8b2123fbaf605e52282b1b33c61da0d09efcc691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Franco?= Date: Wed, 19 Aug 2026 18:37:47 +0200 Subject: [PATCH 2/3] fix(security): url_allowlist_required pass, tests and coverage --- tests/Unit/DependencyInjection/ConfigurationTest.php | 10 ++++++++++ .../DependencyInjection/NowoQrCodeExtensionTest.php | 9 +++++++++ .../UrlAllowlistValidationPassTest.php | 11 +++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php index 3adec90..bd94d06 100644 --- a/tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -37,6 +37,7 @@ public function testDefaultConfiguration(): void $this->assertSame(10, $config['profiles']['default']['margin']); $this->assertSame('high', $config['profiles']['default']['error_correction']); $this->assertSame([], $config['profiles']['default']['url_allowlist']); + $this->assertFalse($config['url_allowlist_required']); $this->assertFalse($config['use_database_config']); $this->assertSame('', $config['doctrine']['table_prefix']); $this->assertSame(['ROLE_ADMIN'], $config['security']['access_roles']); @@ -133,6 +134,15 @@ public function testNamedProfiles(): void $this->assertSame(['nowo.tech'], $config['profiles']['compact']['url_allowlist']); } + public function testUrlAllowlistRequiredFlag(): void + { + $config = $this->processor->processConfiguration($this->configuration, [[ + 'url_allowlist_required' => true, + ]]); + + $this->assertTrue($config['url_allowlist_required']); + } + public function testUnknownDefaultProfileIsRejected(): void { $this->expectException(InvalidConfigurationException::class); diff --git a/tests/Unit/DependencyInjection/NowoQrCodeExtensionTest.php b/tests/Unit/DependencyInjection/NowoQrCodeExtensionTest.php index b406ad3..8e70224 100644 --- a/tests/Unit/DependencyInjection/NowoQrCodeExtensionTest.php +++ b/tests/Unit/DependencyInjection/NowoQrCodeExtensionTest.php @@ -41,6 +41,7 @@ public function testLoadRegistersServicesAndParameters(): void $this->assertSame(10, $container->getParameter('nowo_qr_code.margin')); $this->assertSame('high', $container->getParameter('nowo_qr_code.error_correction')); $this->assertSame([], $container->getParameter('nowo_qr_code.url_allowlist')); + $this->assertFalse($container->getParameter('nowo_qr_code.url_allowlist_required')); $this->assertSame('@NowoQrCodeBundle/admin/layout.html.twig', $container->getParameter('nowo_qr_code.web_ui.layout_template')); $this->assertSame(CssFramework::Custom->value, $container->getParameter('nowo_qr_code.web_ui.css_framework')); } @@ -335,4 +336,12 @@ public function testLoadNamedProfiles(): void $this->assertSame(200, $container->getParameter('nowo_qr_code.size')); $this->assertSame(['pay.google.com'], $container->getParameter('nowo_qr_code.url_allowlist')); } + + public function testLoadSetsUrlAllowlistRequiredParameter(): void + { + $container = new ContainerBuilder(); + $this->extension->load([['url_allowlist_required' => true]], $container); + + $this->assertTrue($container->getParameter('nowo_qr_code.url_allowlist_required')); + } } diff --git a/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php b/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php index 73ffb1a..7770d8d 100644 --- a/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php +++ b/tests/Unit/DependencyInjection/UrlAllowlistValidationPassTest.php @@ -34,7 +34,7 @@ public function testPassesWhenRequiredAndAllowlistNonEmpty(): void (new UrlAllowlistValidationPass())->process($container); - self::assertTrue(true); + $this->expectNotToPerformAssertions(); } public function testPassesWhenNotRequiredAndAllowlistEmpty(): void @@ -45,6 +45,13 @@ public function testPassesWhenNotRequiredAndAllowlistEmpty(): void (new UrlAllowlistValidationPass())->process($container); - self::assertTrue(true); + $this->expectNotToPerformAssertions(); + } + + public function testSkipsWhenAllowlistParameterMissing(): void + { + (new UrlAllowlistValidationPass())->process(new ContainerBuilder()); + + $this->expectNotToPerformAssertions(); } } From 94aa2630cde9bebb1fa2fd3b976dff9ef2d5678e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Franco?= Date: Wed, 19 Aug 2026 18:37:57 +0200 Subject: [PATCH 3/3] style: sort use imports --- src/NowoQrCodeBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NowoQrCodeBundle.php b/src/NowoQrCodeBundle.php index f791fd6..1db1a99 100644 --- a/src/NowoQrCodeBundle.php +++ b/src/NowoQrCodeBundle.php @@ -6,8 +6,8 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; use Nowo\QrCodeBundle\DependencyInjection\Compiler\TwigPathsPass; -use Nowo\QrCodeBundle\DependencyInjection\UrlAllowlistValidationPass; use Nowo\QrCodeBundle\DependencyInjection\NowoQrCodeExtension; +use Nowo\QrCodeBundle\DependencyInjection\UrlAllowlistValidationPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle;