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-srcfor app code and vendor SDKsstyle-srcfor UI frameworks and inline styles from older pluginsconnect-srcfor APIs, telemetry, chat, and streamingframe-srcfor embedded tools and videoform-actionfor SSO and external POST targetsframe-ancestorsto control who can embed your LMSbase-uriandobject-srcto 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'instyle-srcmore than I tolerate it inscript-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-dynamicobject-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 portalsframe-srcwill need more approved domains for LTI tools, video, and webinar providersform-actionoften needs IdP and SSO endpointsconnect-srcusually grows fast in real-time learning products
Policy pattern: LMS with video, analytics, and consent tooling
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-Onlybefore enforce mode?
My default recommendation
For most LMS teams:
- Keep the main app strict.
- Isolate legacy course content and SCORM on another origin.
- Use nonce-based
script-src. - Explicitly model
frame-src,connect-src, andform-action. - Be very deliberate about
frame-ancestors. - 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.