Educational platforms are a CSP minefield.

A normal marketing site might load a few scripts, an analytics tag, and maybe a video embed. An LMS loads SSO flows, SCORM packages, grading tools, PDFs, discussion widgets, proctoring apps, webinar platforms, analytics, and random third-party course content written by people who have never heard of CSP.

So the trick is not “make CSP strict at all costs.” The trick is building a policy that blocks obvious abuse without breaking the actual learning experience.

Here’s the reference guide I wish more LMS teams had.

What makes CSP hard in LMS environments

Educational platforms usually need some combination of:

  • first-party app UI
  • admin dashboards
  • embedded video
  • LTI tools
  • SCORM/xAPI packages
  • SSO redirects and form posts
  • file previews
  • analytics and consent tooling
  • live classes over WebSocket or WebRTC
  • user-generated HTML in course content

That means your policy often needs to cover:

  • script-src for app code and vendor SDKs
  • style-src for UI frameworks and inline styles from older plugins
  • connect-src for APIs, telemetry, chat, and streaming
  • frame-src for embedded tools and video
  • form-action for SSO and external POST targets
  • frame-ancestors to control who can embed your LMS
  • base-uri and object-src to shut down old-school injection tricks

If you only copy a generic CSP from a blog post, you’ll break half your platform.

Start with a sane LMS baseline

This is a practical starting point for a modern LMS that serves its own assets and embeds a few approved services.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self';
  frame-src 'self';
  form-action 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'self';
  upgrade-insecure-requests;
  report-to csp-endpoint;
  report-uri /csp-report;

A few opinions here:

  • I like object-src 'none' on basically everything.
  • I use base-uri 'self' by default because <base> tag abuse is still a real thing.
  • I prefer nonces over host allowlists for scripts when possible.
  • I tolerate 'unsafe-inline' in style-src more than I tolerate it in script-src. Old LMS themes and WYSIWYG editors often force your hand.

A real CSP example worth studying

Here’s a real header captured from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-Zjk4MWRhMzktN2FkMS00YjNiLWE1MDgtNTViNTU3ZWMyM2Ni' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://app.tallytics.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

What I like:

  • nonce-based script-src
  • strict-dynamic
  • object-src 'none'
  • base-uri 'self'
  • explicit connect-src
  • frame-ancestors 'none' for a site that should not be embedded

What I’d adapt for an LMS:

  • frame-ancestors 'none' is usually too strict if schools embed parts of the platform in portals
  • frame-src will need more approved domains for LTI tools, video, and webinar providers
  • form-action often needs IdP and SSO endpoints
  • connect-src usually grows fast in real-time learning products

This is a common setup for course delivery.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic'
    https://www.googletagmanager.com
    https://*.cookiebot.com
    https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline'
    https://*.cookiebot.com
    https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self'
    https://api.example-edu.com
    https://*.google-analytics.com
    https://*.googletagmanager.com
    https://*.cookiebot.com
    wss://live.example-edu.com;
  frame-src 'self'
    https://consentcdn.cookiebot.com
    https://www.youtube-nocookie.com
    https://player.vimeo.com;
  media-src 'self' https:;
  form-action 'self';
  frame-ancestors 'self' https://portal.school.example;
  base-uri 'self';
  object-src 'none';

This is the kind of policy I’d deploy first, then tighten once I know what the platform actually uses.

Policy pattern: LMS with SSO and external identity providers

SSO is where teams forget form-action and then spend an afternoon wondering why login broke.

If your LMS posts to an IdP, add it explicitly.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://login.microsoftonline.com https://accounts.google.com;
  form-action 'self' https://login.microsoftonline.com https://accounts.google.com;
  frame-src 'self';
  frame-ancestors 'self';
  base-uri 'self';
  object-src 'none';

If your SSO uses hidden iframes for silent token refresh, you may also need those domains in frame-src.

Policy pattern: LMS with embedded LTI tools

LTI integrations are usually iframe-heavy and messy. You need a clear allowlist.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://tool1.example https://tool2.example;
  frame-src 'self' https://tool1.example https://tool2.example;
  form-action 'self' https://tool1.example https://tool2.example;
  frame-ancestors 'self' https://school-portal.example;
  base-uri 'self';
  object-src 'none';

My advice: keep LTI domains separated in config, not hardcoded in templates. These integrations change constantly.

SCORM and legacy content: the ugly part

SCORM packages are where strict CSP dreams go to die.

A lot of SCORM content expects:

  • inline scripts
  • inline styles
  • eval()-like behavior
  • assets loaded from weird relative paths
  • popup windows
  • frame communication

If you host SCORM under the same origin as your main LMS app, you’re making your security posture worse. I strongly prefer isolating SCORM on a separate subdomain with its own looser CSP.

Main app:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

Legacy SCORM host:

Content-Security-Policy:
  default-src 'self' data: blob: https:;
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
  style-src 'self' 'unsafe-inline' https:;
  img-src 'self' data: blob: https:;
  font-src 'self' data: https:;
  connect-src 'self' https:;
  frame-ancestors 'self' https://lms.example.edu;
  base-uri 'self';
  object-src 'none';

Do I love this? No. But I’d rather isolate bad legacy content than weaken the whole LMS.

Handling user-generated course content

If instructors can paste HTML into lessons, assume they will paste embed codes, inline styles, and occasionally dangerous junk.

CSP helps, but don’t treat it as your sanitizer.

You still need server-side or trusted rendering rules. Then use CSP to reduce blast radius.

A safer pattern is:

  • sanitize user HTML
  • render it in a restricted container or iframe
  • use a separate origin if possible
  • give that origin a narrower CSP

Example for a course-content subdomain:

Content-Security-Policy:
  default-src 'none';
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  frame-src https://www.youtube-nocookie.com https://player.vimeo.com;
  media-src https:;
  font-src 'self' data:;
  base-uri 'none';
  object-src 'none';
  frame-ancestors 'self' https://lms.example.edu;

Notice there’s no script-src here. That’s deliberate.

Common directives LMS teams should not ignore

frame-ancestors

This controls who can embed your platform.

If your LMS should only appear inside your school portal:

frame-ancestors 'self' https://portal.example.edu;

If it should never be embedded:

frame-ancestors 'none';

For login pages and admin panels, I’m usually stricter than for course players.

connect-src

This breaks APIs, live chat, analytics, attendance polling, and video signaling when it’s wrong.

Typical LMS example:

connect-src 'self' https://api.example.edu wss://realtime.example.edu https://analytics.example.edu;

If your live classroom uses WebSockets, don’t forget wss: endpoints.

form-action

Needed for SSO, payment flows, and some exam vendors.

form-action 'self' https://idp.example.edu https://payments.example.edu;

base-uri

Use it.

base-uri 'self';

Or lock it down harder:

base-uri 'none';

Report-Only first, especially in production LMS systems

A school semester is not the time to discover your CSP blocks quiz submissions.

Start with report-only:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  object-src 'none';
  base-uri 'self';
  report-uri /csp-report;

Watch violations, separate noise from real breakage, then enforce.

Official docs for the header and directives are here:

If you want ready-made policy patterns, https://csp-examples.com is handy.

A solid production checklist

Before shipping CSP on an educational platform, I usually check these:

  • Does login still work with all IdPs?
  • Do embedded videos render?
  • Do LTI tools load in iframes?
  • Do quizzes and assignment uploads still submit?
  • Do real-time classes connect over wss://?
  • Does mobile web behave differently from desktop?
  • Are admin pages and learner pages using the same policy when they shouldn’t?
  • Can SCORM be isolated on another origin?
  • Are nonces generated per response and actually applied to inline scripts?
  • Are we using Report-Only before enforce mode?

My default recommendation

For most LMS teams:

  1. Keep the main app strict.
  2. Isolate legacy course content and SCORM on another origin.
  3. Use nonce-based script-src.
  4. Explicitly model frame-src, connect-src, and form-action.
  5. Be very deliberate about frame-ancestors.
  6. Roll out with reporting first.

That’s the difference between a CSP that looks nice in a compliance spreadsheet and one that survives a real semester.