OGE logoOGE

CSP and Trusted Types

OGE UI runs under a strict Content Security Policy: no inline scripts, no eval, no string-built DOM, and two named Trusted Types policies for the only two parsers in the suite. The policy below is the one the end-to-end suite serves this site under.

strict-dynamic nonce require-trusted-types-for

What the packages need

CSP directives and what OGE UI needs from them
DirectiveOGE UI needsWhy
script-src'self' (or a nonce with 'strict-dynamic' ) No inline scripts, no eval , no new Function , no document.write . Export libraries load as ordinary lazy chunks.
style-src'self' + a nonce (Angular) or 'unsafe-inline' Angular adds component <style> elements; React ships a stylesheet file.
style-src-attr'unsafe-inline' Layout values in server-rendered style attributes (virtual-scroll offsets, panel positions).
img-src'self' data: blob: Upload previews and image exports use object URLs; the editor allows data:image/* only when enabled.
font-src'self' PDF exports embed only a font you serve ( setOgePdfDefaultFont() ); nothing is fetched from a CDN.
connect-src your API only Components fetch nothing on their own; remote data goes through your DataSource .
require-trusted-types-for'script' + the policy names below Two parsers, each behind a named policy.

A static host cannot mint a per-request nonce. There, allow the component styles inline and keep scripts strict:

# a static host cannot mint a nonce — allow component styles inline
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob:;
  object-src 'none';
  base-uri 'self'

A tested strict policy

apps/dev-app-e2e/ssr/strict-csp.spec.ts serves the prerendered site with this header (a fresh nonce per request) and fails on any securitypolicyviolation while it hydrates the pages, exports a grid to CSV and Excel (the lazy export chunk loads under 'strict-dynamic'), imports BPMN XML, renders an HTML overlay badge, opens a modal in both render layers and exports a chart to JPEG and PDF.

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{NONCE}' 'strict-dynamic';
  style-src 'self' 'nonce-{NONCE}';
  style-src-attr 'unsafe-inline';
  img-src 'self' data: blob:;
  font-src 'self' data:;
  connect-src 'self';
  worker-src 'self' blob:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';
  require-trusted-types-for 'script';
  trusted-types angular angular#bundler oge-ui#bpmn oge-ui#editor

Nonces

Angular adds a <style> element per component stylesheet, OGE's included. Give Angular the request's nonce and it stamps it on each of them — the strict run injects it as ngCspNonce on the root element, the way a nonce-injecting SSR server or edge function would:

<!-- index.html as your server sends it: the same per-request nonce as the header.
     Angular stamps it on the <style> elements it adds for component styles. -->
<app-root ngCspNonce="{NONCE}"></app-root>
// …or provide it in code (e.g. read from a meta tag your server writes)
import { ApplicationConfig, CSP_NONCE } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: CSP_NONCE,
      useFactory: () =>
        document.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]')
          ?.content ?? null,
    },
  ],
};

Server-rendered layout values (virtual-scroll offsets, popup positions) arrive as inline style attributes, hence style-src-attr 'unsafe-inline'; they cannot run script.

Trusted Types

Under require-trusted-types-for 'script' the suite has exactly two sinks, both DOMParser.parseFromString into an inert document that is only walked, never inserted. Each engine creates its policy once, lazily, the first time it parses.

Trusted Types policy names
PolicyCreated byWhen
oge-ui#editor@oge-ui/behavior (used by @oge-ui/editor , @oge-ui/react-editor ) Parsing a value or a clipboard payload in the rich-text editor.
oge-ui#bpmn@oge-ui/bpmn-engine Importing BPMN XML, and overlay badge markup in the React layer.
angular Angular Its sanitizing [innerHTML] (BPMN overlay badges in the Angular layer). Angular apps only.
angular#bundler Angular Loading lazy chunks. Angular apps only.

The suite never calls a bypassSecurityTrust* API, so angular#unsafe-bypass is not needed. A new sink must reuse a documented policy or add one to SECURITY.md — the strict run reads its list of names from that file, so an undocumented policy fails it.

URLs from data

Data-driven links (menu items, breadcrumbs, menubar items) can carry a javascript: URL. Angular's [href] binding neutralises it; React's href does not, so every React component routes data-driven href/src through sanitizeUrl / sanitizeResourceUrl from @oge-ui/behavior. Use the same functions for links you render yourself:

URL sanitizer behaviour
InputsanitizeUrlsanitizeResourceUrl
/orders/42 , https://… , mailto: , tel: , ftp: , sms: kept kept
javascript: , vbscript: , unknown schemes about:blankabout:blank
blob:… , data:image/png;…about:blank kept
data:text/html;… (markup) about:blankabout:blank
a custom scheme with allowedSchemes: ['web+app'] kept about:blank (no options)
'use client';

import { sanitizeUrl } from '@oge-ui/behavior';

interface Link {
  readonly label: string;
  readonly url: string;
}

// links from a CMS or an API: javascript: and other script schemes become about:blank
export function CmsLinks({ links }: { links: readonly Link[] }) {
  return (
    <ul>
      {links.map((link) => (
        <li key={link.label}>
          <a href={sanitizeUrl(link.url, { allowedSchemes: ['web+app'] })}>
            {link.label}
          </a>
        </li>
      ))}
    </ul>
  );
}

Rich text

The rich-text editor is the one component whose value is markup. The bound value, every paste and drop, and insertHtml() go through one allowlist parser into the editor's document model: allow-listed tags, href (sanitizeUrl), src (sanitizeResourceUrl; no blob:, file: or SVG; data:image/* only with allowDataImages), alt, title, dir, validated colours and text-align. Everything else — script, style, svg, iframe, event handlers — is dropped, and the emitted value is re-serialized from the model. ogeSanitizeEditorHtml() applies the same allowlist to HTML you render elsewhere, in the browser or on a server:

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ogeSanitizeEditorHtml } from '@oge-ui/editor';

@Component({
  selector: 'demo-root',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <article [innerHTML]="html"></article>
  `,
})
export class Demo {
  // stored rich text, rendered outside the editor: the same allowlist the editor
  // applies to its value, pastes and drops (scripts, styles, svg, iframes and
  // event handlers are dropped; href/src go through sanitizeUrl)
  readonly html = ogeSanitizeEditorHtml(
    '<p>Hello <a href="javascript:alert(1)">there</a><script>steal()</script></p>',
  );
}

Client-side sanitizing is a convenience, not your server's defence: still sanitize stored HTML on the server, as you would any user-written markup. The same goes for upload rules — see SECURITY.md for exports (CSV formula guard), restored state and the reporting process.

What is not covered

  • The strict run does not drive the rich-text editor yet; its oge-ui#editor policy is documented and listed in the policy, but no end-to-end test types into the editor under require-trusted-types-for.
  • The scheduler's print() adds a print stylesheet to its print frame as a <style> element without a nonce; under a nonce-only style-src that sheet is blocked and the printout keeps the on-screen scroll heights.
  • The production docs site itself uses the static-host shape (script hashes, 'unsafe-inline' styles) — see the SSR guide for the one inline script event replay needs.