Problem/Motivation
On any Drupal site running Klaro with a Content Security Policy that does not include 'unsafe-eval' in script-src, the consent banner fails to render. Root cause: the Klaro JS library calls new Function("opts", e) for every service's on_init, on_accept, on_decline, and callback_code field, even when those fields are empty strings. The empty-string default set by the module's default service configs triggers the eval path, which the browser blocks under CSP, throwing an error inside the Manager constructor and preventing the banner from ever rendering.

This is not an edge case: it is the out-of-the-box experience for any site that enables default services (ga, youtube, gtm, cms, and others) and follows the standard CSP hardening recommendation of omitting 'unsafe-eval'.

Root cause detail
The Klaro JS bundle's applyConsents() method contains this helper (verified in libraries/klaro/dist/klaro-no-translations-no-css.js at byte offset 50862, in the file shipped by drupal/klaro_js:3.1.0 which wraps upstream klaro-js v0.7.22):

function o(e, t) {
if (void 0 === e) return;
let n;
return n = "function" == typeof e ? e : new Function("opts", e), n(t)
}
The intent: skip when the callback is undefined; if it is a function, call it directly; otherwise compile the string to a function and call that.

The bug: void 0 === "" evaluates to false. An empty string is not undefined, so the early return does not fire. Execution falls through to new Function("opts", ""), which under a strict CSP throws an EvalError. The error propagates out of the Manager constructor (new zt at byte offset 48626), so the manager instance is never created and the banner never renders.

The Drupal module writes empty strings ("") rather than NULL for unset callback fields. Verified in the config export of default-enabled services on a live installation:

klaro.klaro_app.ga (enabled):
callback_code: EMPTY string ("")
on_init: EMPTY string ("")
on_accept: EMPTY string ("")
on_decline: EMPTY string ("")

klaro.klaro_app.youtube (enabled):
callback_code: EMPTY string ("")
on_init: EMPTY string ("")
on_accept: EMPTY string ("")
on_decline: EMPTY string ("")

# same for cms, klaro services
Every default-enabled service triggers the new Function() call the first time applyConsents() iterates over it. On the very first initialization, this crashes the constructor before any banner rendering.

Why this matters
Default install pattern: enabling any built-in service triggers the crash. Users don't need to write custom callbacks to hit this bug.
Best-practice conflict: omitting 'unsafe-eval' from CSP is the OWASP and MDN-recommended hardening. Klaro effectively requires users to weaken their CSP to a specific documented anti-pattern to make the module work.
Silent failure mode: the banner just does not appear. Users think Klaro is misconfigured, not that the browser blocked it. Dblog logs the CSP violation, but many admins do not correlate that entry with the missing banner.
Precedent in the changelog: version 3.0.9 introduced fix #3567998 ("Errors in callback code should not kill the Klaro consent banner"). This report describes the same class of defect but caused by the empty-string default rather than by malformed user code.
Steps to reproduce
Install the Klaro module 3.1.1 on Drupal 11.
Enable one or more of the default services shipped by the module (e.g. ga, youtube, cms).
Do not edit the default callback fields. They remain empty strings, as installed.
Configure a Content Security Policy on the site (Seckit module, or manual header) with script-src set to something like 'self' 'unsafe-inline' https://www.googletagmanager.com. Do not add 'unsafe-eval'.
Open the site in an incognito window.
Expected: cookie consent banner appears.

Actual: banner does not appear. Browser Console shows:

Uncaught EvalError
at new zt (klaro-no-translations-no-css.js:1)
at Module.kn (klaro-no-translations-no-css.js:1)
at Object.proceed (js_HASH.js)
at Object.attach (js_HASH.js)
at Array.forEach
at Drupal.attachBehaviors
Browser DevTools Issues panel reports: "Content Security Policy of your site blocks the use of 'eval' in JavaScript", pointing to klaro-no-translations-no-css.js.

Drupal Watchdog (dblog) logs a Seckit CSP violation event with blocked-uri: eval and source-file: /libraries/klaro/dist/klaro-no-translations-no-css.js.

Proposed resolution
Two independent fixes, either is sufficient. Ideally both, so the issue is closed at both layers.

Fix A: Drupal module side (this issue)
The module should filter out empty callback fields before passing the service config to Klaro JS. In the module's config-to-JS bridge (likely in Klaro\Utility\KlaroHelper or the render/attach logic), for each service:

// Before writing to the JS config array:
foreach (['callback_code', 'on_init', 'on_accept', 'on_decline'] as $field) {
if (isset($serviceConfig[$field]) && $serviceConfig[$field] === '') {
unset($serviceConfig[$field]);
}
}
Result: empty callback fields become undefined in the JS-side config, klaro-js's if (void 0 === e) return correctly short-circuits, no new Function() call is made, and the banner renders correctly even under strict CSP.

Backwards compatibility: none broken. Empty-string callbacks never produced meaningful behavior anyway (they compile to no-op functions). Users who have actual callback code will still have their strings passed through untouched.

Fix B: klaro-js upstream (report separately)
Report to https://github.com/klaro-org/klaro-js/issues. The bundle's helper should treat empty strings the same as undefined:

function o(e, t) {
if (!e) return; // catches undefined, null, "", 0, false
let n;
return n = "function" == typeof e ? e : new Function("opts", e), n(t)
}
One-character conceptual change (void 0 === e becomes !e), fully backwards compatible.

Workaround for affected sites (documented, not recommended long-term)
Add 'unsafe-eval' to the site's script-src CSP directive. Example using the Seckit module:

drush cset seckit.settings seckit_xss.csp.script-src \
"'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com"
drush cr
This makes the browser tolerate the new Function() call. Klaro's helper then compiles empty strings to no-op functions, which are called without effect, and the banner renders.

Note the security tradeoff: 'unsafe-eval' widens the site's XSS surface. Sites that have gone through the effort of establishing a strict CSP now have to weaken it to accommodate Klaro. The workaround should be temporary until either Fix A or Fix B lands.

Remaining tasks
Decide whether Fix A (module-side) or Fix B (upstream) is preferred as the primary fix, or apply both.
Write a test that enables a default service without CSP relaxation and asserts the banner renders.
Update module documentation to note the CSP interaction (until fix lands).
User interface changes
None.

API changes
None if Fix A is applied at the config-to-JS bridge; the change is internal to the module.

Data model changes
None.

Environment
Drupal 11.3.x
PHP 8.3
Klaro module 3.1.1
drupal/klaro_js 3.1.0 (wrapping klaro-js v0.7.22)
Seckit 2.0.x
Verified reproducible on two independent installations, same versions, same crash-stack, same byte offsets in the bundle.

Issue fork klaro-3607716

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

askfred created an issue. See original summary.

avpaderno’s picture

Version: 3.1.1 » 3.1.x-dev
Issue tags: -csp, -Security, -klaro-js, -callback, -new Function, -unsafe-eval

daniel groen made their first commit to this issue’s fork.