Skip to content

Security: BenjaminWard-Python/simple-lms

Security

SECURITY.md

Security

Reporting a vulnerability

Please open a private security advisory through GitHub rather than a public issue.


The one thing you must understand before using this

Uploading a course is equivalent to running code on your site.

SCORM packages are ordinary HTML, CSS and JavaScript. To work at all, a package must run in the browser and reach the LMS runtime (window.API) in its parent page, which means it runs on the same origin as the application itself, unsandboxed. That is inherent to SCORM 1.2, not a shortcut taken here.

The consequence: a malicious or compromised package can do anything the signed-in person's browser can do on your site. It can read the pages they can read and make requests as them. Session cookies are HttpOnly, so a package cannot read the cookie itself — but it does not need to, because the browser attaches it to every request the package makes.

Practical rules:

  1. Only upload packages from vendors you trust. Treat a SCORM zip like a browser extension, not like a PDF.
  2. Only give administrator rights to people you would trust to deploy code. Course upload is an administrator capability, and it is a powerful one.
  3. Be especially careful with packages from a customer or a third party. "Can you load this course for us" is a request to run their code on your infrastructure.

Every self-hosted SCORM LMS shares this property. Commercial hosts mitigate it by serving package content from a separate domain so a package cannot reach the application's own API. If you need that isolation, serve /api/scorm/* from a different hostname behind your proxy and adjust contentUrl in src/app/learn/[id]/page.tsx accordingly. It is not the default because it requires a second hostname, which would stop the simple setup working out of the box.


What the application does protect

Passwords are hashed with scrypt and a per-user salt. They are never emailed: new accounts and resets both use a one-time link, and only a SHA-256 hash of that link's token is stored, so a copy of the database does not yield working links. Links expire — a week for an invitation, two hours for a reset — and are single use.

Sessions are stateless, signed with HMAC-SHA256 over a server secret. The cookie is HttpOnly, SameSite=Lax, and marked Secure whenever the connection is really HTTPS, detected from x-forwarded-proto rather than assumed. SameSite=Lax is what protects state-changing requests from cross-site forgery.

Sign-in is rate limited per account and per address: eight failures against one account, or twenty-five from one address, pause attempts for fifteen minutes.

Nothing that changes state answers a GET. Sign-out, sign-in, password changes and organization switching are all POST. This is not pedantry: an earlier version had a GET /logout, and link prefetching signed users out seconds after they arrived.

Tenants are isolated at every query. An administrator only ever sees their own organization's people and records. Only a platform-owner account can move between organizations, and doing so is written to the activity log.

Package files are served with a path-traversal guard and only to someone actually assigned that course, or an administrator of the organization that owns it. Uploads are extracted with a zip-slip guard.

The activity log is append-only. Nothing in the application updates or deletes a row in it.

Diagnostic endpoints (/api/session-debug, /api/cookie-test) return 404 unless DEBUG_TOKEN is set and supplied. Leave it unset in normal operation.


What it does not do

  • No two-factor authentication. If you need it, put an identity-aware proxy in front.
  • No audit of administrator reads. Writes are logged; simply viewing a report is not.
  • No encryption at rest beyond whatever your host provides. The SQLite file and the extracted packages are ordinary files.
  • No virus scanning of uploads. Package contents are served back as-is.
  • Completion records are as trustworthy as the course reporting them. The application records what the SCORM runtime is told; a package can tell it anything. This is why point 1 above matters.

There aren't any published security advisories