Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,46 @@ All notable changes to ReadingBat Core are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [3.2.0] - Unreleased
## [3.2.0] - 2026-06-15

A security-hardening release. A multi-agent security review surfaced 48 confirmed findings (7 high, 19 medium, 22 low); all 48 are addressed here, alongside the build/tooling cleanup below.

### Security

- Signed and encrypted all three session cookies (browser session, auth principal, OAuth return URL) with `SessionTransportTransformerEncrypt` (AES-128 + HMAC-SHA256). Previously the cookies were unsigned plaintext, so the `userId` could be forged for a trivial authentication bypass and full admin takeover
- Introduced a required `SESSION_SECRET` in production: the server refuses to start without it. It must be shared across all nodes; rotating it invalidates existing cookies. Generate with `openssl rand -hex 32`
- Enforced class-ownership authorization on teacher class-management actions via `ownsClass()`, closing an IDOR that let any teacher modify any class (including remove-from-class)
- Eliminated server-side code injection (RCE): Python/Kotlin answer comparison no longer interpolates the user response into an evaluated script — list/array answers are parsed and compared directly
- Fixed stored XSS in the teacher dashboard: student answers streamed over WebSockets are now HTML-escaped before rendering
- Fixed JS injection via the student full name in the remove-from-class `confirm()` handler and the other user-prefs `onSubmit` handlers (escaped with `escapeEcmaScript`)
- Enforced rate limiting with a global per-IP limiter (the RateLimit plugin was installed but never applied)
- Rotate the browser session cookie on login to prevent session fixation
- Require a verified email for both Google and GitHub OAuth logins; GitHub no longer creates blank-email user accounts
- Mask secrets in logs instead of revealing 75% of their characters
- Require admin access to subscribe to the operational logging WebSocket

### Fixed

- Bounded the answer-check loop by the challenge's invocation count, so an attacker-supplied response count can no longer trigger an `IndexOutOfBoundsException`
- Startup no longer crashes when an integer environment variable holds a non-numeric value
- WebSocket reliability: fixed concurrent-modification crashes in the ping/clock pinger loops, replaced the single shared dispatcher that head-of-line-blocked all clients, and collapsed N+1 blocking JDBC calls in WS coroutines; the answer-dashboard flow now drops the oldest message under backpressure instead of suspending the answer-submit handler
- DSL parser fixes: nested-brace handling in `convertToKotlinScript`, and source-line ordering in `extractJavaInvocations` (invocations now align with computed answers)
- `extractJavaFunction` reports a named "malformed challenge" error instead of an opaque crash when source has fewer than two `static` declarations
- Geo cache: short-circuits the DB on a cached hit, no longer serializes all lookups behind a global mutex, no longer caches transient failures permanently, and is now size-bounded (LRU)
- Fixed a dir-contents cache key mismatch that caused a permanent miss plus a leak
- Fixed an enrollment-rollback desync, an OAuth-link insert race (now an upsert), and a blocking fetch performed under a `computeIfAbsent` lock
- Quote-aware parsing of Python list answers so elements containing commas are no longer split and corrupted
- Background content-load failures are logged (with the failing source) instead of leaving the server permanently "not ready"
- Coalesced the request-logging path into a single transaction (was 3–4 transactions per request)

### Changed

- `Challenge.functionInfo()` is now `suspend`: the blocking JSR-223 script eval no longer bridges through `runBlocking` inside request/WS coroutines and runs on `Dispatchers.IO` (depends on the suspend-block `respondWith` in common-utils 2.9.2)
- Bounded the per-user answer-queue channels with a fixed-size worker pool keyed by user id (was an unbounded map of channels/coroutines)
- Per-property initialization tracking so the config "not initialized" error reflects the specific property
- Converted `upsert()` calls to Exposed's native `upsert()` and dropped the local upsert overload
- Deduplicated repeated `fetchClassTeacherId()` lookups in the class/student authorization paths and removed dead code (the `repo` getter guard, an unused content-root sentinel)
- Extracted shared WebSocket client JS (origin rewrite + summary `onmessage`) into `PageUtils`, and split `displayStudentProgress` into a data pass and a render pass
- Deduplicated the `/oauth` URL literal: introduced `Endpoints.OAUTH_PREFIX` and derived `OAUTH_LOGIN_*` / `OAUTH_CALLBACK_*` from it; `Intercepts.publicPrefixes` now references `OAUTH_PREFIX` instead of a duplicated string
- Trimmed redundant `"/static/"` entries from `publicPrefixes` and `readinessAllowedPrefixes` (already covered by `"/$STATIC/"`)
- Removed the unused `"/css.css"` entry from `Intercepts.publicPaths`
Expand All @@ -26,6 +62,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `scripts/coverage_packages.py` — extracted the inline Python in `make coverage-packages` into a standalone script for readability and reuse
- Makefile private helper targets (`_check-gpg-env`, `_require-version`, `_require-gradle-version`) replacing inline `ifeq` error guards; publish targets now declare them as prerequisites

### Dependencies

- common-utils → 2.9.2 (suspend-block `respondWith`/`redirectTo`, native Exposed `upsert`, fixed upsert conflict indexes)
- detekt 2.0.0-alpha.3 → 2.0.0-alpha.4
- HikariCP 7.0.2 → 7.1.0
- Kotest 6.1.11 → 6.2.0
- prometheus-proxy 3.1.1 → 3.2.0

## [3.1.8] - 2026-05-04

### Changed
Expand Down
14 changes: 10 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,14 @@ The app uses a two-layer configuration pattern where most settings can come from
### Authentication

- Form-based auth with salted password hashes and session cookies
- Session cookies are signed and encrypted (`SessionTransportTransformerEncrypt`, AES-128 + HMAC-SHA256) in
`ConfigureCookies.kt`. This requires a `SESSION_SECRET` (env var) that is **mandatory in production** — the server
will not start without it. It must be identical on every node (so cookies validate across instances), and rotating it
invalidates all existing sessions. Generate one with `openssl rand -hex 32`.
- The browser session is rotated on login to prevent session fixation.
- OAuth (GitHub, Google) via `ConfigureOAuth` — providers are auto-configured when credentials are present in env vars,
not gated on production mode. The `OAuthProvider` enum in `OAuthRoutes.kt` provides type-safe provider identification.
OAuth logins require a verified email from the provider.

### Page Generation

Expand Down Expand Up @@ -153,9 +159,9 @@ The `readingbat-kotest` module provides `TestSupport` with helpers:

### Key Dependencies

- **common-utils** 2.8.2 (BOM from `com.github.pambrose`): shared utility library providing core-utils, email-utils,
exposed-utils, ktor-client/server-utils, script-utils, etc.
- **prometheus-proxy** 3.1.1: metrics collection
- **Kover** 0.9.1: code coverage, applied to every subproject and aggregated at the root; CI uploads
- **common-utils** 2.9.2 (BOM from `com.github.pambrose`): shared utility library providing core-utils, email-utils,
exposed-utils, ktor-client/server-utils, script-utils, etc. (`respondWith`/`redirectTo` take a `suspend` block as of 2.9.2)
- **prometheus-proxy** 3.2.0: metrics collection
- **Kover** 0.9.8: code coverage, applied to every subproject and aggregated at the root; CI uploads
`build/reports/kover/report.xml` to Codecov via `codecov-action@v5`
- Dependency versions managed in `gradle/libs.versions.toml`
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@
[![codecov](https://codecov.io/gh/readingbat/readingbat-core/branch/master/graph/badge.svg)](https://codecov.io/gh/readingbat/readingbat-core)
[![Kotlin](https://img.shields.io/badge/%20language-Kotlin-red.svg)](https://kotlinlang.org/)
[![ktlint](https://img.shields.io/badge/ktlint%20code--style-%E2%9D%A4-FF4081)](https://pinterest.github.io/ktlint/)
[![Docs](https://img.shields.io/badge/docs-readingbat.github.io-blue)](https://readingbat.github.io/readingbat-core/)

A Kotlin-based framework for creating interactive programming challenges and educational content, powering the
[ReadingBat](https://readingbat.com) platform for teaching Java, Kotlin, and Python programming concepts.

📖 **Documentation:** <https://readingbat.github.io/readingbat-core/>

## 🚀 Features

- **Multi-Language Support**: Create challenges for Java, Kotlin, and Python
- **Interactive DSL**: Expressive domain-specific language for defining programming exercises
- **Web-Based Platform**: Built on Ktor with real-time WebSocket updates
- **User Management**: Complete authentication system with class/teacher support
- **Progress Tracking**: Detailed analytics and progress monitoring
- **Security**: Signed + encrypted session cookies, per-IP rate limiting, verified-email OAuth, and class-ownership authorization
- **Scalable Architecture**: Multi-server deployment ready with database persistence

## 🏗️ Architecture

ReadingBat Core is built using modern Kotlin technologies:

- **Web Framework**: Ktor 3.4.3 with CIO engine
- **Web Framework**: Ktor 3.5.0 with CIO engine
- **Database**: PostgreSQL with Exposed ORM (`exposed-kotlin-datetime`) and HikariCP connection pooling
- **Authentication**: OAuth (GitHub, Google) with session management
- **Authentication**: OAuth (GitHub, Google, verified-email required) with signed + encrypted session cookies
- **Script Execution**: JSR-223 scripting engines for safe code evaluation
- **Build System**: Gradle 9.5 with Kotlin DSL, multi-module structure, and configuration cache enabled
- **Serialization**: kotlinx.serialization for JSON processing
Expand Down Expand Up @@ -227,6 +231,7 @@ ReadingBat Core includes comprehensive monitoring:

## 📚 Related Projects

- **[Documentation site](https://readingbat.github.io/readingbat-core/)**: DSL, configuration, server, testing guides, and release notes
- **[ReadingBat Template](https://github.com/readingbat/readingbat-template)**: Template for creating custom content

## 📄 License
Expand Down
36 changes: 33 additions & 3 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
# Release Notes

## v3.2.0 — Unreleased
## v3.2.0 — 2026-06-15

Cleanup release: dedupes the `/oauth` URL literal, trims redundant entries from the auth/readiness allowlists, adds a Codecov configuration, centralizes toolchain versions in the version catalog, migrates detekt to the 2.0 alpha line, and tightens the Makefile.
Security-hardening release. A multi-agent security review surfaced 48 confirmed findings (7 high, 19 medium, 22 low); all 48 are addressed here. The headline items close an authentication bypass, an RCE, two IDOR/authorization gaps, and stored/reflected XSS, plus a batch of WebSocket and caching reliability fixes. The 3.1.9-era build/tooling cleanup ships in the same release.

### Highlights
### ⚠️ Upgrade note — `SESSION_SECRET` is now required in production

Session cookies are now signed and encrypted, which means the server **will not start in production without a `SESSION_SECRET`**. Generate one with:

```
openssl rand -hex 32
```

Set it via the `SESSION_SECRET` environment variable. It must be the **same value on every node** (so cookies validate across instances), and **rotating it invalidates all existing sessions** (users must log in again).

### Security highlights

- **Signed + encrypted session cookies.** All three session cookies use `SessionTransportTransformerEncrypt` (AES-128 + HMAC-SHA256). The cookies were previously unsigned plaintext, so the `userId` could be hand-edited for a trivial authentication bypass and full admin/sysadmin takeover.
- **Server-side code injection (RCE) removed.** Python/Kotlin answer checking no longer interpolates the user's response into an evaluated script; list/array answers are parsed and compared directly.
- **Teacher IDOR closed.** Class-management actions (including remove-from-class) now verify class ownership via `ownsClass()`.
- **XSS fixes.** Student answers streamed over WebSockets are HTML-escaped before rendering, and user-controlled names in `confirm()` / `onSubmit` handlers are escaped with `escapeEcmaScript`.
- **OAuth hardening.** Google and GitHub logins require a verified email; GitHub no longer creates blank-email accounts. Login rotates the browser session to prevent fixation.
- **Rate limiting enforced** with a global per-IP limiter (previously installed but never applied); secrets are masked in logs; the operational logging WebSocket requires admin.

### Reliability highlights

- **WebSocket robustness.** Fixed concurrent-modification crashes in the pinger loops, removed a single shared dispatcher that head-of-line-blocked every client, collapsed N+1 blocking JDBC in WS coroutines, and made the answer-dashboard flow drop-oldest under backpressure instead of suspending the submit handler.
- **Caching fixes.** Geo cache now short-circuits the DB, drops its global mutex, stops permanently caching failures, and is size-bounded; a dir-contents cache key mismatch (permanent miss + leak) is fixed; per-user answer-queue channels are bounded by a fixed worker pool.
- **Parser + startup fixes.** Nested-brace Kotlin script conversion, Java invocation ordering, quote-aware Python list parsing, a non-numeric env-var startup crash, and a bounded answer-check loop (no more attacker-triggered `IndexOutOfBoundsException`).
- **`Challenge.functionInfo()` is now `suspend`** — the blocking script eval no longer bridges through `runBlocking` inside request/WS coroutines and runs on `Dispatchers.IO`.

### Build & tooling highlights

- **`/oauth` URL deduped.** New `Endpoints.OAUTH_PREFIX` constant; `OAUTH_LOGIN_*` and `OAUTH_CALLBACK_*` endpoints derive from it, and `Intercepts.publicPrefixes` references the constant instead of a duplicated literal.
- **Allowlist cleanup.** Removed the redundant `"/static/"` entries from `publicPrefixes` / `readinessAllowedPrefixes` (already covered by `"/$STATIC/"`) and the unused `"/css.css"` entry from `publicPaths`.
Expand All @@ -15,6 +41,10 @@ Cleanup release: dedupes the `/oauth` URL literal, trims redundant entries from
- **Kotlin serialization plugin wired explicitly.** `readingbat-core/build.gradle.kts` now applies `libs.plugins.kotlin.serialization` via the catalog alias so the compiler plugin is in effect for the subproject (root keeps the `apply false` declaration).
- **Makefile tightening.** New `make help` target prints a self-documenting index of every target with a `## description` annotation, and a bare `make` invokes it. `make lint` now runs Kotlinter and detekt in a single Gradle invocation instead of double-running detekt via a prerequisite. The inline Python in `make coverage-packages` moves to `scripts/coverage_packages.py`. Inline `ifeq` version guards become explicit prerequisite targets (`_check-gpg-env`, `_require-version`, `_require-gradle-version`), and `$GPG_SIGNING_KEY_ID` is properly quoted in the signing block.

### Dependencies

common-utils → 2.9.2 · detekt → 2.0.0-alpha.4 · HikariCP → 7.1.0 · Kotest → 6.2.0 · prometheus-proxy → 3.2.0

**Full Changelog**: https://github.com/readingbat/readingbat-core/compare/3.1.8...3.2.0

---
Expand Down
16 changes: 11 additions & 5 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ ReadingBat Core is a web application built with Ktor that serves programming cha

## Key Technologies

- Kotlin 2.3.21, Ktor 3.4.3 (CIO engine), Exposed ORM 1.2.0 with kotlinx-datetime, PostgreSQL
- Kotlin 2.4.0, Ktor 3.5.0 (CIO engine), Exposed ORM 1.3.0 with kotlinx-datetime, PostgreSQL
- Kotlinx.html for server-side HTML generation (no templates)
- Kotest for testing, Playwright for E2E tests
- Gradle 9.5.0 multi-module build with version catalog (libs.versions.toml) as the single source of truth for plugin, dependency, and toolchain (`gradle`, `jvm`) versions; parallel project execution and configuration cache enabled
- Kover 0.9.1 for code coverage, uploaded to Codecov from CI (per-area components defined in `codecov.yml`)
- Kotest 6.2.0 for testing, Playwright for E2E tests
- Gradle 9.5.1 multi-module build with version catalog (libs.versions.toml) as the single source of truth for plugin, dependency, and toolchain (`gradle`, `jvm`) versions; parallel project execution and configuration cache enabled
- Kover 0.9.8 for code coverage, uploaded to Codecov from CI (per-area components defined in `codecov.yml`)
- Detekt 2.0 alpha (`dev.detekt` plugin) for static analysis; Kotlinter for ktlint style
- common-utils 2.9.2 (shared utility BOM from `com.github.pambrose`)
- Java 17 toolchain

## Project Structure
Expand Down Expand Up @@ -60,7 +61,11 @@ Type-safe routing via Ktor `@Resource` annotations in `Locations.kt`: Language -

## Authentication

OAuth (GitHub, Google) with session management. OAuth providers are auto-configured when credentials are present (not gated on production mode). Provider identification uses the `OAuthProvider` enum in `common/` for type safety.
OAuth (GitHub, Google) with session management. OAuth providers are auto-configured when credentials are present (not gated on production mode). Provider identification uses the `OAuthProvider` enum in `common/` for type safety. OAuth logins require a verified email; the browser session is rotated on login to prevent fixation.

## Security

Session cookies are signed and encrypted (`SessionTransportTransformerEncrypt`, AES-128 + HMAC-SHA256). This requires a `SESSION_SECRET` (env var), which is **mandatory in production** — the server will not start without it. It must be shared across all nodes, and rotating it invalidates existing sessions. Generate one with `openssl rand -hex 32`. Answer checking parses and compares values directly (no script-engine eval of user input); user-controlled strings are HTML/JS-escaped before rendering; rate limiting is enforced per-IP; secrets are masked in logs. As of 3.2.0 a multi-agent security review of 48 findings (auth, IDOR, RCE, XSS, reliability) was fully addressed.

## Value Types

Expand All @@ -82,5 +87,6 @@ Project version, group, and `releaseDate` are defined in `gradle.properties`. Ov

## Links

- [Documentation](https://readingbat.github.io/readingbat-core/)
- [Source](https://github.com/readingbat/readingbat-core)
- [Template Repository](https://github.com/readingbat/readingbat-template)
9 changes: 9 additions & 0 deletions website/readingbat-core/docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,21 @@ Create your secrets file:

```bash
# secrets/secrets.env
SESSION_SECRET=<openssl rand -hex 32>
DBMS_PASSWORD=my-database-password
GITHUB_OAUTH_CLIENT_SECRET=ghp_xxxxxxxxxxxx
GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxx
RESEND_API_KEY=re_xxxxxxxxxxxx
```

!!! warning "`SESSION_SECRET` is required in production"

Session cookies are signed and encrypted (AES-128 + HMAC-SHA256), so a
`SESSION_SECRET` **must** be set in production — the server will not start
without it. Generate one with `openssl rand -hex 32`, use the **same value on
every node** (so cookies validate across instances), and note that **rotating
it invalidates all existing sessions** (users must log in again).

## Property Initialization

Properties are initialized during application startup in `Application.module()`:
Expand Down
Loading
Loading