# Non-essential scripts only load after consent is granted for their category

- **Pattern:** `ab-000801` (`cookie-consent-compliance.consent-enforcement.scripts-conditional-on-consent`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000801
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000801)

## Why it matters

ePrivacy Art. 5(3) prohibits accessing or storing information on a device without prior consent. GDPR Art. 6(1)(a) requires a lawful basis — and for non-essential tracking, consent is the only available basis. When GA4, Facebook Pixel, or Segment loads via a static `<script>` tag or an unconditional `strategy='afterInteractive'` Next.js `<Script>`, it fires before any user interaction, collecting device identifiers and behavioral data without a lawful basis. CCPA §1798.135 imposes the parallel obligation for California residents' right to opt out of sale/sharing of personal information, which ad pixels facilitate. This is the most commonly litigated technical failure in DPA enforcement actions.

## Severity rationale

Critical because a static or unconditionally loaded analytics script fires on every page view, collecting personal data without any lawful basis under GDPR Art. 6(1)(a) and ePrivacy Art. 5(3) — the core violation regulators fine for.

## Remediation

Either use GA4 Consent Mode v2 with `analytics_storage: 'denied'` as the default (the only pattern that allows GA4 in the static layout), or move all other non-essential scripts into a `loadApprovedScripts()` function called only after the user saves consent.

```tsx
// GA4 Consent Mode v2 — set the denied default BEFORE the gtag script tag
<Script id="consent-defaults" strategy="beforeInteractive">{`
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('consent', 'default', {
    analytics_storage: 'denied',
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
  });
`}</Script>
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX" strategy="afterInteractive" />
```

For scripts without Consent Mode support (Segment, Hotjar, Facebook Pixel), inject them into the DOM only inside the consent callback in `src/lib/consent-scripts.ts`. Never place them as static `<Script>` tags with `strategy='afterInteractive'` — that strategy is not consent-aware.

## Detection

- **ID:** `scripts-conditional-on-consent`
- **Severity:** `critical`
- **What to look for:** This is the most technically important check in the audit. Find every non-essential script that the application loads. For each one, trace its loading path: (1) Is it a static `<script>` tag in the HTML `<head>`? If so, it always loads — this is a violation unless it is a consent-exempt essential script. (2) Is it a Next.js `<Script>` component? Check its `strategy` prop: `strategy="beforeInteractive"` or `strategy="afterInteractive"` loads without consent; `strategy="lazyOnload"` still loads without consent. None of these strategies are consent-aware. (3) Is it dynamically appended to the DOM after a consent callback? This is the correct pattern. (4) Is it imported as a JavaScript module (`import mixpanel from 'mixpanel-browser'`) and initialized unconditionally at module load time? This is a violation — initialization must be inside a consent check. Check GA4 specifically: does the layout contain a `gtag('consent', 'default', { analytics_storage: 'denied' })` call before the GA4 script loads? GA4 Consent Mode v2 is acceptable — it defers measurement until consent is granted — but requires correct setup.
- **Pass criteria:** Count all third-party script tags and tracking pixels in the codebase. Every non-essential script (analytics, pixels, marketing tools, A/B testing, chat widgets) is either: (a) dynamically loaded only after the user's consent for its category is `true`, OR (b) using an official consent mode integration (e.g., GA4 Consent Mode v2) with `analytics_storage: 'denied'` as the default state. No non-essential script tag appears statically in the HTML layout without a consent gate. 100% of non-essential scripts must be conditional on user consent. Report the count of conditional vs unconditional scripts even on pass.
- **Fail criteria:** Should not pass when any analytics or marketing script loads before the user has given consent. Analytics or tracking scripts appear as static `<script>` tags in the layout `<head>` without a consent gate. Scripts are initialized at module level without a consent check. GA4 is present but Consent Mode v2 is not configured — raw measurement fires on page load regardless of consent.
- **Skip (N/A) when:** Application loads no non-essential third-party scripts.
- **Detail on fail:** Specify the unconsented script(s). Example: `"GA4 script tag present in app/layout.tsx head with no consent gate. Fires on every page load before any user interaction."` or `"Facebook Pixel fbq() initialized unconditionally in pages/_app.tsx at module level, outside any consent check."` or `"Segment analytics.js loads via static Script strategy='afterInteractive' — loads without checking consent state."`.
- **Remediation:** Move all non-essential script loading into consent callbacks. For GA4, use Consent Mode v2 as an alternative:

  ```tsx
  // GA4 Consent Mode v2 — initialize BEFORE the GA4 script tag
  // This is the only pattern that lets you put GA4 in the layout safely
  // app/layout.tsx head section:

  // 1. Set default denied state (no measurement until consent)
  <Script id="consent-mode-default" strategy="beforeInteractive">
    {`
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('consent', 'default', {
        analytics_storage: 'denied',
        ad_storage: 'denied',
        ad_user_data: 'denied',
        ad_personalization: 'denied',
      });
    `}
  </Script>

  // 2. Then load the GA4 library (it respects the denied default)
  <Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX" strategy="afterInteractive" />

  // 3. In your consent save handler, update consent state:
  // gtag('consent', 'update', { analytics_storage: 'granted' })
  ```

  For all other scripts (Facebook Pixel, Segment, Hotjar, etc.) that do not support Consent Mode, use the dynamic loading pattern:

  ```ts
  // lib/consent-scripts.ts
  export function loadApprovedScripts(consent: ConsentState) {
    if (consent.analytics) {
      // Load Segment only after consent
      import('@segment/analytics-next').then(({ AnalyticsBrowser }) => {
        const analytics = AnalyticsBrowser.load({ writeKey: process.env.NEXT_PUBLIC_SEGMENT_KEY! })
        window.__analytics = analytics
      })
    }
    if (consent.marketing) {
      // Load Facebook Pixel only after consent
      const script = document.createElement('script')
      script.textContent = `
        !function(f,b,e,v,n,t,s){/* fbq snippet */}
        fbq('init', 'YOUR_PIXEL_ID');
        fbq('track', 'PageView');
      `
      document.head.appendChild(script)
    }
  }
  ```


- **Cross-reference:** For third-party script security and CSP configuration, see the Security Headers audit.
---

## External references

- eprivacy Art. 5(3)
- gdpr Art. 6(1)(a)
- ccpa §1798.135
- cwe CWE-284

Taxons: privacy-consent, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-000801
