Optional integrations build inline <script> elements, forcing script-src 'unsafe-inline'

Open
#6,078 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Quiet
Tech stack
angular, typescript
Domain
frontend, security

Research direction

Start with the listed script creation sites in src/app/statistics/google-analytics.service.ts, src/app/core/shared/client-math.service.ts, src/app/core/google-recaptcha/google-recaptcha.service.ts, src/app/social/social.service.ts, and src/app/statistics/matomo.factory.ts; check how #6077 exposes CSP_NONCE. Remove inline script assignments and apply the configured nonce to dynamic scripts, then verify the acceptance criteria across all five integrations, including behavior without a nonce.

Written by the indexing model from the issue text.

Description

needs discussion new feature security

Is your feature request related to a problem? Please describe.

Two of the optional integrations construct <script> elements with inline content at runtime.
Any deployment that enables them therefore requires script-src 'unsafe-inline', which is the
single directive value that most undermines a Content-Security-Policy.

Google Analytics (src/app/statistics/google-analytics.service.ts:71-85) builds the config
snippet as inline script for both the GTag and the legacy variant:

const libScript = this.document.createElement('script');
libScript.innerHTML = `window.dataLayer = window.dataLayer || [];function gtag(){window.dataLayer.push(arguments);}
                     gtag('js', new Date());gtag('config', '${trackingId}');`;
this.document.body.appendChild(libScript);

MathJax (src/app/core/shared/client-math.service.ts:83-86) does the same for its options
object:

const optionsScript: HTMLScriptElement = this._document.createElement('script');
optionsScript.type = 'text/javascript';
optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`;
this._document.head.appendChild(optionsScript);

In both cases the inline script does nothing that could not be done directly in TypeScript. It
assigns a global. Routing it through a <script> element buys nothing and costs the ability to
run under a meaningful CSP.

Separately, several services inject external scripts at runtime, which is fine in itself but
becomes relevant under a nonce-based policy that does not use 'strict-dynamic', since a
dynamically created script element carries no nonce:

Service Location
Google Analytics / GTag src/app/statistics/google-analytics.service.ts:67
Google reCAPTCHA src/app/core/google-recaptcha/google-recaptcha.service.ts:182
MathJax src/app/core/shared/client-math.service.ts:88
AddToAny social sharing src/app/social/social.service.ts:82
Matomo src/app/statistics/matomo.factory.ts:19-24

Describe the solution you'd like

1. Remove the two inline script blocks. Assign the globals directly instead of via a script
element:

// MathJax: set the global before loading the library, no script element needed
(window as any).MathJax = this.mathJaxOptions;

// Google Analytics: same, define dataLayer and gtag in TypeScript

This alone removes the need for script-src 'unsafe-inline' on deployments that enable Analytics
or MathJax, and is worth doing independently of any nonce work.

2. Apply the nonce to dynamically created script elements. Once #6077 provides CSP_NONCE,
each of the services above should set script.nonce = inject(CSP_NONCE) on the element it creates.
This is a no-op when no nonce is configured, so it is backwards compatible, and it makes the
integrations work under an explicit-allowlist policy rather than depending on 'strict-dynamic'.

A small shared helper (for example createScriptElement(doc, src, nonce)) would avoid repeating
this in five places and would make it harder for a future integration to reintroduce the problem.

Acceptance criteria

  • No innerHTML or .text assignment on a <script> element remains in src/.
  • Every dynamically created <script> element carries the nonce when one is configured,
    verifiable in the DOM with Analytics, Matomo, MathJax, reCAPTCHA and AddToAny all enabled.
  • With those five enabled and a policy of script-src 'nonce-X' 'strict-dynamic' (no
    'unsafe-inline', no 'unsafe-eval'), all five load and function with zero CSP violations.
  • Existing behaviour is unchanged when no nonce is configured.

Describe alternatives or workarounds you've considered

  • Rely on 'strict-dynamic'. Because these scripts are created via createElement rather than
    by the HTML parser, 'strict-dynamic' would permit them as-is, including the inline ones. That
    is the pragmatic first step proposed in #6077, but it means trusting anything application code
    loads, so it should not be the permanent answer.
  • Allowlist the hosts in the policy. Works for the external scripts but not the inline ones, and
    several of the hosts are runtime configuration rather than build-time constants (see phase 3 in
    #6079).

Additional information

Umbrella: #6079. Depends on #6077 only for part 2; part 1 is independent and can be merged
first.

Dominant language
TypeScript
Stars
180
Forks
577
Avg merge
2d 1h
Merged PRs (30d)
100

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from DSpace/dspace-angular

All issues in DSpace/dspace-angular

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.