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.
| Version | Supported |
|---|---|
| 1.0.x | ✅ |
| < 1.0 | ❌ (pre-release) |
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.
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
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/ defaultadmincredentials 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=truewith/metricsexposed to the internet). - automated-scanner output with no demonstrated impact.
'unsafe-inline'instyle-src— a known, documented limitation with a written rationale (see architecture.md). A new approach to removing it is welcome as a pull request.
What the platform does, and what it deliberately does not.
| 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 |
- 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.
/metricshas 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.
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_COUNTto the real number of proxies - keep
SEED_DEMO_USERfalse - keep
/metricsoff the internet