The SQL Playground is a unique feature that lets users query CTF data using SQL. It runs entirely in the browser using DuckDB WASM, making it safe and performant.
DuckDB WASM is loaded from a CDN (jsDelivr). Browser security policies block loading web workers from external domains on localhost, causing a CORS error.
Docker containers properly handle CORS, so the SQL Playground works out-of-the-box:
docker compose -f docker-compose.dev.yml upThen visit: http://localhost:8090/sql
Why this works: Docker's network setup bypasses the localhost CORS restrictions.
Download DuckDB WASM files locally so they're served from your own server:
task setup-sqlscripts/setup-duckdb-local.bat# Create directory
mkdir -p internal/views/static/duckdb
# Download files
curl -L https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-mvp.wasm \
-o internal/views/static/duckdb/duckdb-mvp.wasm
curl -L https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-browser-mvp.worker.js \
-o internal/views/static/duckdb/duckdb-browser-mvp.worker.jsThen build and run:
task build
task runVisit: http://localhost:8090/sql
How it works:
- App tries to load DuckDB from CDN first (best for production)
- If that fails, falls back to local files
- Shows which source is being used in the UI
The SQL Playground works perfectly in production because real domains don't have CORS restrictions:
task build-prod
# Deploy binary to your server┌─────────────────────────────────────────────────────┐
│ Browser (Client-Side) │
├─────────────────────────────────────────────────────┤
│ │
│ 1. Load DuckDB WASM (from CDN or local) │
│ ↓ │
│ 2. Fetch CTF data (/api/sql/snapshot) │
│ ↓ │
│ 3. Create in-memory database │
│ ↓ │
│ 4. Execute user queries (NO server access) │
│ ↓ │
│ 5. Display results │
│ │
└─────────────────────────────────────────────────────┘
-
User types SQL query →
SELECT * FROM challenges WHERE difficulty = 'hard' -
Query executes in browser using DuckDB WASM
- 100% client-side
- No SQL injection risk (runs in sandbox)
- No server load
-
Results displayed as HTML table
The playground can query these tables:
| Table | Fields | Source |
|---|---|---|
| challenges | id, name, category, difficulty, visible, created_at | Public challenges only |
| questions | id, challenge_id, name, points, flag_mask | Without actual flags |
| submissions | id, question_id, user_id, is_correct, created_at | Correct submissions only |
| users | id, name, team_id, created_at | Public user info |
Find hardest challenges:
SELECT c.name, c.difficulty, COUNT(s.id) as solves
FROM challenges c
LEFT JOIN questions q ON c.id = q.challenge_id
LEFT JOIN submissions s ON q.id = s.question_id AND s.is_correct = 1
GROUP BY c.id, c.name, c.difficulty
ORDER BY solves ASC
LIMIT 10Top users by points:
SELECT u.name, SUM(q.points) as total_points, COUNT(*) as solve_count
FROM users u
JOIN submissions s ON u.id = s.user_id
JOIN questions q ON s.question_id = q.id
WHERE s.is_correct = 1
GROUP BY u.id, u.name
ORDER BY total_points DESC
LIMIT 10Challenge difficulty distribution:
SELECT difficulty, COUNT(*) as count
FROM challenges
WHERE visible = 1
GROUP BY difficultyUser progress:
SELECT
u.name,
COUNT(DISTINCT s.question_id) as questions_solved,
COUNT(*) as total_attempts,
MAX(s.created_at) as last_attempt
FROM users u
LEFT JOIN submissions s ON u.id = s.user_id
GROUP BY u.id, u.nameProblem: DuckDB failed to load from both CDN and local fallback
Solutions:
- Check internet connection
- Run
task setup-sqlto download local files - Try Docker:
docker compose -f docker-compose.dev.yml up - Check browser console (F12) for detailed error message
This is normal! The app will:
- Show "✅ Database loaded (from Local)"
- SQL Playground will work fine
- It means you're using the local fallback
DuckDB WASM is very fast, but:
- First load takes ~2-3 seconds (loading WASM module)
- Subsequent queries are instant
- Works best with <50MB of data (typical CTF size)
| Feature | DuckDB | sql.js | Server-side |
|---|---|---|---|
| Features | Full SQL, CTEs, window functions | Basic SQL | Full SQL |
| Performance | Excellent | Good | Excellent |
| Security | Sandboxed | Sandboxed | Depends |
| Server Load | None | None | High |
| Complexity | Medium | Low | High |
We use DuckDB because it offers the best combination of features and performance.
- Snapshot loaded once when page opens
- Reflects data at that moment
- Refresh page to get latest data
- Good for learning; use
/api/challengesfor real-time data
| Browser | Status |
|---|---|
| Chrome | ✅ Works |
| Firefox | ✅ Works |
| Safari | ✅ Works |
| Edge | ✅ Works |
| IE 11 | ❌ Not supported |
Edit internal/database/queries.go → GetSQLSnapshot() to control what data is exposed.
Edit internal/views/templates/sql.html → exampleQueries array to add more examples.
The DuckDB module has a default timeout. To customize, edit the script in sql.html.
✅ Why it's safe:
- SQL runs in browser (no server access)
- User can only query public data
- Cannot modify data (read-only)
- No SQL injection risk (DuckDB handles it)
- No sensitive data exposed (passwords, flags hidden)
- Cannot delete/update/insert data
- Cannot access tables from other connections
- Cannot execute arbitrary JavaScript (sandbox)
| Step | Time | Notes |
|---|---|---|
| Load DuckDB WASM | 2-3s | First time only |
| Load snapshot data | 500ms | Typical CTF data |
| Parse into DB | 100ms | Creates tables |
| Simple query | 10ms | Most queries |
| Complex query | 50-200ms | Joins, aggregations |
- DuckDB module: 5-10MB
- Data snapshot: 1-5MB (varies)
- Total: ~10-15MB for typical CTF
- Real-time data updates (WebSocket)
- Query history/save favorite queries
- Query optimization hints
- Visual query builder
- Export results to CSV
- Query execution analytics
- Support for team-specific queries
SQL Playground not working?
- Try Docker first (most reliable)
- Run
task setup-sqlfor local files - Check browser console (F12) for errors
- Ensure internet connection is stable
Want to contribute?
- Add new example queries
- Improve documentation
- Report bugs
- Suggest features