ddtcorex

04 Sept 2026 · 3 min read

Case study: a social-login widget that ignored its own config

A Hyva + Alpine social-login module rendered the wrong buttons, dropped hidden ones, and tripped CSP. The fix was Alpine reactivity, not security policy.

Field notes — a real production bug, project name withheld. Symptom → investigation → fix → lesson.

The symptom

A third-party social-login module, re-skinned for a Hyva theme, had a cluster of small but real failures: the "show more" toggle did nothing, hidden providers stayed on screen, clicking a button sometimes navigated to a blank page, and the browser console lit up with CSP violations sourced from the widget's inline script. Individually minor; together they made the login chooser unusable and flagged on the security console.

Investigation

The widget was an Alpine component. Reading the template, several methods it called no longer existed, and the ones that remained were bound in ways Alpine does not track:

// methods referenced by the template that were removed
isButtonVisible(btn) { return btn.visible || this.show; }
getButtonClasses(btn) { return btn.code + (...); }

// and the template still called them:
<li x-show="isButtonVisible(btn)" :class="getButtonClasses(btn)">
    <a @click.prevent="login(btn)" ...>

isButtonVisible and getButtonClasses had been deleted from the component, so the bound expressions silently failed — Alpine left the elements in their last state, which is why hidden buttons showed and the class list never updated. The @click.prevent="login(btn)" passed the whole object; after the refactor login expected an event, not a button, so btn.url was undefined and the handler bailed.

The account list had the same shape: templates read account.profileURL directly inside x-if, but the data was never shaped to guarantee that key, so the branch picked the wrong render path.

On top of the logic bugs, the widget's inline <script> registered itself for CSP only when a method ran — meaning on pages where the script loaded but the old method path was taken, the inline script was never whitelisted and CSP blocked it.

Root cause

Two classes of mistake. First, non-reactive bindings: the template referenced methods that had been removed and bound data keys that weren't guaranteed to exist. Alpine renders once against what's defined; missing pieces don't error, they just don't work. Second, conditional, not guaranteed, CSP registration: an inline script must be whitelisted declaratively, not as a side effect of a method that may or may not run.

The fix

Make the component data-driven and reactive. Pre-shape the data so the template only reads stable keys, and turn methods into getters (or remove them in favor of bound properties):

function initPsloginLinkedNetworks() {
    let networks = <?= /* @noEscape */ $linksPrepared ?>;
    const baseUrl = '<?= $escaper->escapeJs($block->getViewFileUrl('images/')) ?>';
    return {
        accounts: networks.map(account => {
            account.hasProfileURL = !!account.profileURL;
            account.hasNoProfileURL = !account.profileURL;
            account.iconSrc = baseUrl + '/' + account.type + '_icon.svg';
            account.altText = 'icon ' + account.icon;
            return account;
        }),
        unlink(event) {
            const account = this.accounts.find(a => a.type === event.currentTarget.dataset.type);
            if (account) { window.location = account.unlinkURL; }
        }
    };
}

For the buttons, resolve the target from the event and bind a stable property instead of a method call:

<li x-show="btn.isVisible" :class="btn.linkClass" :title="btn.text">
    <a @click.prevent="login" :data-code="btn.code" ...>
btn.linkClass = btn.code + btn.loginClass;
btn.isVisible = !!btn.visible;
// ...
login(event) {
    const btn = this.buttons.find(button => button.code === event.currentTarget.dataset.code);
    if (!btn || !btn.url) { /* guard + message */ return false; }
    // proceed with btn.url
}

And the inline script is registered unconditionally at the point it is emitted, inside the PHP guard:

<?php isset($hyvaCsp) && $hyvaCsp->registerInlineScript() ?>

Lesson

Alpine binds to what is defined at render time. If the template references a method or a data key that isn't there, you don't get an error — you get a silently dead widget. Shape your data up front, use getters for derived state, and pass events (not objects) to handlers so they can resolve the real target. CSP for inline scripts is a declaration, not a side effect: register it where the script is written, or it will block exactly when you least expect. Roll CSP out in report-only mode first and watch the console per route (home, category, product, checkout) before enforcing.

magento2hyvafrontendcspalpine