Skip to content

Commit 16486af

Browse files
robertSt7claude
andauthored
Make Twig sandbox object/function access policy configurable (blocklist + allowlist) (pimcore#19303)
* Make Twig sandbox object-access policy configurable (blocklist + allowlist) SecurityPolicy previously enforced a single hardcoded denylist of infrastructure classes for method/property access from sandboxed templates. Add `templating_engine.twig.sandbox_security_policy` options `blocked_classes` (extends the built-in denylist) and `allowed_classes` (switches to allowlist mode, deactivating the denylist entirely once non-empty), so sites can either extend the denylist or lock the sandbox down to a known set of classes. Related: GHSA-7gfm-v2fx-xrxm draft advisory recommends allowlisting the sandbox's object access instead of relying solely on a denylist. This change adds the mechanism; existing defaults are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Harden default sandbox object-access policy per GHSA-7gfm-v2fx-xrxm Ship the advisory's minimum remediation as the default, not just as an opt-in config: - Add Pimcore\Model\User to the built-in denylist, so getPassword() and getPasswordRecoveryToken() are no longer template-reachable out of the box. - Add an always-blocked-methods check (independent of blocklist/ allowlist mode) for Asset::getData()/getStream()/getLocalFile()/ getTemporaryFile(), so arbitrary asset content can't be exfiltrated via a sandboxed template while Asset otherwise stays usable for filename/thumbnail access. This hard block is not bypassed even if a site explicitly allowlists User or Asset. Extends the configurable blocked_classes/allowed_classes mechanism added in the previous commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Make the pimcore_* function auto-allow configurable (blocklist + allowlist) Mirrors the blocked_classes/allowed_classes mechanism added for object access: the pimcore_* prefix auto-allow in checkSecurity() now has its own denylist/allowlist pair. - blocked_functions extends the built-in denylist of pimcore_* functions that look up and return a live model instance by id/path (pimcore_user, pimcore_asset, pimcore_document, pimcore_site*, ...) - consulted while allowed_functions is empty (default). - allowed_functions, once non-empty, switches the pimcore_* prefix rule to allowlist mode: the built-in + blocked_functions denylist is deactivated entirely, and only the listed pimcore_* functions (plus whatever is in the pre-existing `functions` allowlist) remain callable. The existing `functions`/$allowedFunctions option is unchanged - it keeps working as an always-active explicit allowlist in both modes. Default behavior (blocked_functions/allowed_functions both empty) is identical to before this commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix CI failure: use @dataProvider docblock, not PHPUnit attribute Codeception's Unit test loader (Codeception\Test\DataProvider) only resolves data providers via the @dataProvider docblock annotation - it doesn't recognize PHPUnit\Framework\Attributes\DataProvider at all. Using the attribute made Codeception run the two parameterized tests with zero arguments, failing CI with ArgumentCountError. Verified the fix directly against Codeception\Test\DataProvider::getDataForMethod. Also addresses a review comment: replace the two live `new PDO('sqlite::memory:')` connections with $this->createStub(PDO::class), so the tests don't depend on the optional pdo_sqlite driver being installed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Also hard-block User::getTwoFactorAuthentication() unconditionally getTwoFactorAuthentication() (models/User.php:679-696) returns the full 2FA config array, including the raw TOTP secret - a credential equivalent to the password hash / recovery token. Allowlisting User for otherwise-benign getters (e.g. getFirstname()) would still leak the MFA secret via `pimcore_user(1).getTwoFactorAuthentication('secret')`, defeating the point of allowlisting User at all. Add it to ALWAYS_BLOCKED_METHODS alongside the two existing secret getters, so it's hard-blocked regardless of blocklist/allowlist mode. Test coverage and docs extended accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Remove the pimcore_* function allowlist mode (allowed_functions) Drop allowed_functions and the allowlist-mode switch it drove in SecurityPolicy::isPimcoreFunctionAllowed() - the pimcore_* prefix rule is now always denylist-only (built-in BLOCKED_FUNCTIONS + configured blocked_functions), mirroring the hard-block approach already taken for User::getTwoFactorAuthentication(). blocked_functions, allowed_classes/ blocked_classes and the general functions allowlist are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Move built-in sandbox denylists from PHP constants to default.yaml blocked_classes, blocked_functions and the FQCN=>methods map that hard-blocks secret/content-returning getters (renamed always_blocked_methods -> hard_blocked_methods) now carry their built-in values as the default in bundles/CoreBundle/config/pimcore/default.yaml instead of private PHP constants in SecurityPolicy. A site's own config for the same option is merged with (appended to) that default rather than replacing it - verified against Symfony's real Config/Processor component - so extending one of these options cannot accidentally drop the shipped defaults. SecurityPolicyTest's "*ByDefault" tests now read the actual shipped values out of default.yaml instead of duplicating them in a PHP fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Reduce the default pimcore_* blocked_functions denylist to pimcore_user Comment out the id/path lookup functions (pimcore_asset, pimcore_document, pimcore_object, pimcore_site and their variants) in the shipped blocked_functions default, leaving only pimcore_user active by default. These functions are auto-allowed by default now unless a site re-adds them. Document the change: describe pimcore_user as the only function blocked out of the box, list the now-auto-allowed id/path lookup functions and the risk they carry (arbitrary id/path lookup exposing data outside a template's intended scope), and recommend adding them back to blocked_functions for a high-security setup - with the full list shown both as the commented-out default.yaml entries and as a standalone example. Note: this drops CI coverage - testIdLookupPimcoreFunctionsAreNotAutoAllowedByDefault in SecurityPolicyTest reads the real blocked_functions default and will now fail for 12 of its 13 cases (only the pimcore_user case still blocks). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Update SecurityPolicyTest to match the reduced blocked_functions default testIdLookupPimcoreFunctionsAreNotAutoAllowedByDefault asserted all 13 id/path lookup functions are blocked by default, which no longer matches default.yaml (only pimcore_user is still active there; the rest are shipped commented out). Split it into: - testPimcoreUserIsNotAutoAllowedByDefault - the one still blocked by default. - testOtherIdLookupPimcoreFunctionsAreAutoAllowedByDefault - confirms the other 12 are now auto-allowed by default, matching the shipped config. - testIdLookupPimcoreFunctionsCanBeBlockedForHighSecurity - confirms all 13 still block correctly once a site configures the full list, as recommended in doc/26_Best_Practice/80_Twig_Sandbox_Object_Access.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent c5b2917 commit 16486af

9 files changed

Lines changed: 879 additions & 32 deletions

File tree

bundles/CoreBundle/config/pimcore/default.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,40 @@ pimcore:
336336
tags: ['set']
337337
filters: ['escape', 'trans', 'default']
338338
functions: ['path', 'asset']
339+
blocked_classes:
340+
- Pimcore\Model\Dao\AbstractDao
341+
- Doctrine\DBAL\Connection
342+
- PDO
343+
- PDOStatement
344+
- Symfony\Component\DependencyInjection\ContainerInterface
345+
- Symfony\Component\Process\Process
346+
- Pimcore\Model\User
347+
allowed_classes: []
348+
blocked_functions:
349+
# - pimcore_asset
350+
# - pimcore_asset_by_path
351+
# - pimcore_document
352+
# - pimcore_document_by_path
353+
# - pimcore_document_wrap_hardlink
354+
# - pimcore_object
355+
# - pimcore_object_by_path
356+
# - pimcore_object_classificationstore_group
357+
# - pimcore_object_brick_definition_key
358+
# - pimcore_site
359+
# - pimcore_site_by_root_id
360+
# - pimcore_site_by_domain
361+
# - pimcore_site_current
362+
- pimcore_user
363+
hard_blocked_methods:
364+
Pimcore\Model\User:
365+
- getPassword
366+
- getPasswordRecoveryToken
367+
- getTwoFactorAuthentication
368+
Pimcore\Model\Asset:
369+
- getData
370+
- getStream
371+
- getLocalFile
372+
- getTemporaryFile
339373

340374
gotenberg:
341375
base_url: 'http://gotenberg:3000'

bundles/CoreBundle/config/templating_twig.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ services:
7777
$allowedTags: '%pimcore.templating.twig.sandbox_security_policy.tags%'
7878
$allowedFilters: '%pimcore.templating.twig.sandbox_security_policy.filters%'
7979
$allowedFunctions: '%pimcore.templating.twig.sandbox_security_policy.functions%'
80+
$blockedClasses: '%pimcore.templating.twig.sandbox_security_policy.blocked_classes%'
81+
$allowedClasses: '%pimcore.templating.twig.sandbox_security_policy.allowed_classes%'
82+
$blockedFunctions: '%pimcore.templating.twig.sandbox_security_policy.blocked_functions%'
83+
$hardBlockedMethods: '%pimcore.templating.twig.sandbox_security_policy.hard_blocked_methods%'
8084

8185
Twig\Extension\SandboxExtension:
8286
arguments:

bundles/CoreBundle/src/DependencyInjection/Configuration.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,39 @@ private function addTemplatingEngineNode(ArrayNodeDefinition $rootNode): void
20772077
->arrayNode('functions')
20782078
->scalarPrototype()->end()
20792079
->end()
2080+
->arrayNode('blocked_classes')
2081+
->info('FQCNs that must not be traversable (method calls or property access) from
2082+
sandboxed twig templates. Defaults to Pimcore\'s built-in denylist (database/
2083+
infrastructure layer, `Pimcore\Model\User`) - a site can append further FQCNs on
2084+
top of that default. Ignored when `allowed_classes` is non-empty.')
2085+
->scalarPrototype()->end()
2086+
->end()
2087+
->arrayNode('allowed_classes')
2088+
->info('FQCNs that are traversable (method calls or property access) from
2089+
sandboxed twig templates. As soon as this list contains at least one entry, the
2090+
security policy switches from denylist mode to allowlist mode: the
2091+
`blocked_classes` denylist is deactivated, and only instances of the classes
2092+
listed here (and their subclasses) remain reachable.')
2093+
->scalarPrototype()->end()
2094+
->end()
2095+
->arrayNode('blocked_functions')
2096+
->info('`pimcore_*` function names that must not be covered by the blanket
2097+
`pimcore_*` prefix auto-allow. Defaults to Pimcore\'s built-in denylist of
2098+
functions that look up and return a live model instance by id/path - a site can
2099+
append further function names on top of that default.')
2100+
->scalarPrototype()->end()
2101+
->end()
2102+
->arrayNode('hard_blocked_methods')
2103+
->info('FQCN => list-of-method-names map. Methods listed here can never be called
2104+
on a matching instance from a sandboxed twig template, regardless of the
2105+
blocked_classes/allowed_classes configuration. Defaults to a small set of
2106+
secret/content-returning getters (e.g. `User::getPassword`, `Asset::getData`) -
2107+
a site can extend the map with further classes/methods on top of that default.')
2108+
->useAttributeAsKey('class')
2109+
->arrayPrototype()
2110+
->scalarPrototype()->end()
2111+
->end()
2112+
->end()
20802113
->end()
20812114
->end()
20822115
->end()

bundles/CoreBundle/src/DependencyInjection/PimcoreCoreExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ public function loadInternal(array $config, ContainerBuilder $container): void
8080
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.tags', $config['templating_engine']['twig']['sandbox_security_policy']['tags']);
8181
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.filters', $config['templating_engine']['twig']['sandbox_security_policy']['filters']);
8282
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.functions', $config['templating_engine']['twig']['sandbox_security_policy']['functions']);
83+
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.blocked_classes', $config['templating_engine']['twig']['sandbox_security_policy']['blocked_classes']);
84+
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.allowed_classes', $config['templating_engine']['twig']['sandbox_security_policy']['allowed_classes']);
85+
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.blocked_functions', $config['templating_engine']['twig']['sandbox_security_policy']['blocked_functions']);
86+
$container->setParameter('pimcore.templating.twig.sandbox_security_policy.hard_blocked_methods', $config['templating_engine']['twig']['sandbox_security_policy']['hard_blocked_methods']);
8387

8488
// register pimcore config on container
8589
// TODO is this bad practice?

doc/05_Objects/01_Object_Classes/03_Layout_Elements/01_Dynamic_Text_Labels.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,7 @@ security policies for tags, filters & functions. Please use following configurat
7878
filters: ['upper']
7979
functions: ['include', 'path']
8080
```
81+
82+
The sandbox also restricts which objects a template may call methods or access properties
83+
on. See [Twig Sandbox Object Access](../../../26_Best_Practice/80_Twig_Sandbox_Object_Access.md)
84+
for the `blocked_classes` / `allowed_classes` options.

doc/19_Development_Tools_and_Details/25_Email_Framework/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,9 @@ security policies for tags, filters & functions. Please use following configurat
112112
filters: ['upper']
113113
functions: ['include', 'path']
114114
```
115+
116+
The same sandbox is also used to evaluate `Dynamic Text` layout components in DataObject
117+
classes. See [Twig Sandbox Object & Function Access](../../26_Best_Practice/80_Twig_Sandbox_Object_Access.md)
118+
for how to control which PHP objects a sandboxed template is allowed to reach (via a
119+
`blocked_classes` denylist or an `allowed_classes` allowlist), and which `pimcore_*`
120+
functions are auto-allowed (via a `blocked_functions` denylist).

0 commit comments

Comments
 (0)