Skip to content

Security: Joshwa-jd/SecureLearn

Security

SECURITY.md

Security Policy

SecureLearn teaches secure web development, so the application itself is held to the standard it teaches. Reports about the platform's own security are very welcome.

Supported versions

Version Supported
1.0.x ✅
< 1.0 ❌ (pre-release)

Reporting a vulnerability

Please do not open a public issue for a genuine vulnerability.

Use GitHub's private vulnerability reporting (Security → Report a vulnerability). If that is unavailable, open a minimal public issue that says only "security report, please provide a contact" — with no details — and a private channel will be arranged.

Please include:

  • what the issue is and why it matters,
  • steps to reproduce (a request, a payload, a diff),
  • the version or commit,
  • any suggested fix.

Expect: an acknowledgement within 7 days, an assessment within 14, and credit in the changelog when a report leads to a fix — unless you'd rather not be named.

This is a personal educational project with no bug-bounty budget. What is on offer is a prompt fix and public credit.

Scope

In scope — the platform's own code

Anything that would be a real weakness in a deployed instance:

  • authentication, session handling, or authorization flaws
  • injection, XSS, CSRF, IDOR, open redirects
  • a security header, CSP, or cookie-flag regression
  • secrets handling, or a configuration that fails open instead of closed
  • dependency vulnerabilities that are actually reachable from this code
  • anything that lets one user read or change another's data

Out of scope — the teaching content

The vulnerable_code samples in app/content/modules/ are intentional. They are inert illustrations rendered as syntax-highlighted text next to their secure counterpart — the core teaching device of this project. They are never executed, never routed, and never reachable. A report that "SecureLearn contains SQL injection examples" describes the curriculum, not a vulnerability.

Also out of scope:

  • the demo learner / default admin credentials in development. Their passwords are published deliberately; production refuses both (the demo account is not seeded, and seeding aborts on the default admin password).
  • findings that require an insecure deployment the docs explicitly warn against (e.g. METRICS_ENABLED=true with /metrics exposed to the internet).
  • automated-scanner output with no demonstrated impact.
  • 'unsafe-inline' in style-src — a known, documented limitation with a written rationale (see architecture.md). A new approach to removing it is welcome as a pull request.

Security model

What the platform does, and what it deliberately does not.

Controls

Control Implementation
Password storage Salted adaptive hashes (Werkzeug) — never reversible
Session fixation session.clear() on login
Session lifetime session.permanent + 60-minute PERMANENT_SESSION_LIFETIME
Sign-out Session cleared server-side and the remember-me cookie revoked
Remember-me token HttpOnly, SameSite=Lax, Secure in production, 14-day expiry
Account enumeration Login uniform in message and timing — the unknown-user path still hashes
CSRF Flask-WTF CSRFProtect on every form; state changes are never GET
Authorization Deny-by-default role_required; role read from server state, never the request
SQL injection SQLAlchemy ORM throughout — no string-built SQL
XSS Jinja2 autoescape; markdown rendering only for authored content
Open redirect auth._safe_next validates the next parameter
Clickjacking X-Frame-Options: DENY, CSP frame-ancestors 'none'
Script injection Nonce-based CSP — fresh 128-bit nonce per request, no 'unsafe-inline'
Transport HSTS on HTTPS responses only; SESSION_COOKIE_SECURE in production
Brute force Flask-Limiter on login, registration, password change, search
Third-party assets Subresource Integrity on every CDN script and stylesheet
Insecure config Startup refuses a known SECRET_KEY; seeding refuses the default admin password
Forged proxy headers X-Forwarded-* ignored unless TRUSTED_PROXY_COUNT is set
Information disclosure Generic error messages; detail goes to logs only
Container Runs as unprivileged appuser, never root
Supply chain pip-audit on every push; dependency floors are security floors
Static analysis Bandit on every push

Deliberate non-goals

  • No password reset by email. It would add a mail dependency and an account recovery attack surface to a teaching platform that needs neither.
  • No multi-factor authentication. Reasonable for a real product; noted in future-improvements.md.
  • /metrics has no authentication of its own. It is off by default, and the docs require restricting it at the proxy or network layer before enabling.
  • Rate limits are per-process by default. In-memory counters multiply by the worker count; Redis is documented for an accurate shared limit.
  • SQLite by default. Fine for a single instance; PostgreSQL is documented and CI-verified for anything real.

Deploying safely

docs/deployment.md has the full production path and a pre-flight checklist. The essentials:

  • set a strong, unique SECRET_KEY (the app refuses to start otherwise)
  • set a strong SEED_ADMIN_PASSWORD (seeding refuses the default)
  • terminate TLS in front, with SESSION_COOKIE_SECURE=true
  • set TRUSTED_PROXY_COUNT to the real number of proxies
  • keep SEED_DEMO_USER false
  • keep /metrics off the internet

There aren't any published security advisories