ADR-030: Read-time resolution of site-configuration vault references
Table of contents
Status
Accepted
Date
2026-07-23
Context
Site configuration may reference secrets with the
%vault syntax (see
Site configuration). The extension originally
shipped an auto-registered event listener,
SiteConfigurationVaultListener, that resolved those references on
SiteConfigurationLoadedEvent and wrote the resolved array back onto the
event.
That eager path is unsafe because of how TYPO3 core loads site
configuration. SiteConfiguration::getAllSiteConfigurationFromFiles()
dispatches SiteConfigurationLoadedEvent only on a cache miss and then
var_export()s the post-event array — the array the listener has just
filled with decrypted plaintext — into the core cache, which defaults to a
file-backed backend on disk. Every later load is a cache hit that returns the
pre-resolved array without re-dispatching the event. Two guarantees break:
- Encryption at rest. The decrypted secret is written verbatim into
var/cache/code/cache_corein cleartext, so a filesystem read (backup, misconfigured permissions, a second tenant) recovers it without the master key, the database, or any vault access. - Per-reader access control.
VaultService::retrieve()runs itscanRead()/ expiry / audit checks exactly once — in whichever context warms the cache (an authenticated admin request, or a CLI run). A secret that is notfrontend_accessibleis then served from the cache to anonymous frontend requests and to lower-privileged backend users, because the gate never runs again.
The listener also called the processor without the Site object, so it
always took the global-identifier branch and never distinguished
site-scoped secrets.
Decision
Remove SiteConfigurationVaultListener. Resolution of site-configuration
vault references is caller-driven and happens at read time, through
SiteConfigurationVaultProcessor (the entry point already documented in the
class and exposed as a public service):
$site = $request->getAttribute('site');
$processor = GeneralUtility::makeInstance(SiteConfigurationVaultProcessor::class);
$config = $processor->processConfiguration($site->getConfiguration(), $site);
$apiKey = $config['settings']['payment']['apiKey'];
The cached site-configuration array keeps the literal %vault(...)%
placeholders; the plaintext exists only within the request that resolves it,
and canRead() runs for the actual reader on every resolution. Passing the
$site object enables site-scoped identifiers
(site:<siteIdentifier>:<secret>).
Consequences
- Plaintext secrets are never persisted to the on-disk
corecache, and the access decision is enforced per reader rather than once per cache generation. - Behaviour change. Consumers that relied on transparent resolution —
reading
$site->getConfiguration()[...]and receiving a decrypted value — must now callprocessConfiguration()at the point of use. That transparent behaviour was the unsafe path; the explicit call is the supported one. - The processor, its interface, and its public service registration are unchanged; only the eager listener and its tests are removed.
- TypoScript resolution (
TypoScriptVaultListener) is unaffected: it is gated onfrontend_accessibleand documented as making a secret frontend-readable by design.