Skip to main content

Security Guide

This guide documents security practices aligned with OWASP Top 10 2025 and outlines the project's defense-in-depth strategy.

Security Overview

Tutor Nexus implements defense-in-depth security with:

LayerTechnologyOWASP Category
AuthenticationLucia Auth + OAuthA07
AuthorizationRBAC + resource ownershipA01
EncryptionEnvelope encryption (KEK/DEK)A02
Input ValidationZod schemasA03
InfrastructureCloudflare edgeA05
DependenciesDependabot + pnpm auditA06

OWASP Top 10 2025

A01:2025 - Broken Access Control

Access control violations are prevented through:

Role-Based Access Control (RBAC):

// Define roles
const ROLES = {
USER: 'user',
TUTOR: 'tutor',
ADMIN: 'admin',
} as const;

// Check permissions
function requireRole(session: Session, requiredRole: Role) {
if (session.role === ADMIN) return;
if (session.role !== requiredRole) {
throw new AuthorizationError('Insufficient permissions');
}
}

// Protected route
router.use('/admin/*', (c, next) => {
const session = await getSession(c);
requireRole(session, ADMIN);
return next();
});

Resource Ownership:

async function requireOwnership(
db: D1Database,
userId: string,
resourceId: string
): Promise<boolean> {
const resource = await db
.prepare('SELECT user_id FROM resources WHERE id = ?')
.bind(resourceId)
.first();

return resource?.user_id === userId;
}

A02:2025 - Cryptographic Failures

All sensitive data uses envelope encryption ADR-007:

// packages/crypto/src/envelope.ts
import { encrypt, decrypt } from '@tutor-nexus/crypto';

const KEK = await generateKey();

// Encrypt with DEK + KEK
async function encryptCredential(plaintext: string): Promise<Encrypted> {
const dek = await generateDataKey();
const encrypted = await encrypt(plaintext, dek);
const wrapped = await wrapKey(dek, KEK);

return {
ciphertext: encrypted,
wrappedKey: wrapped,
version: CURRENT_VERSION,
};
}

Secrets never logged or exposed in error messages.

A03:2025 - Injection

All input validated with Zod:

import { z } from '@hono/zod-openapi';

const CreateSessionSchema = z.object({
title: z.string().min(1).max(200),
subject: z.string().optional(),
description: z.string().max(2000).optional(),
});

router.post('/sessions', zValidator('json', CreateSessionSchema), (c) => {
const body = c.req.valid('json');
});

Database queries use prepared statements only:

// ✅ Correct - prepared statement
const user = await db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first();

// ❌ Never - string concatenation
const user = await db.prepare(`SELECT * FROM users WHERE id = ${userId}`);

A04:2025 - Insecure Design

Security is addressed at the architecture level through:

Threat modeling performed during ADR review process.

A05:2025 - Security Misconfiguration

Cloudflare Security Features:

# wrangler.toml
[vars]
NODE_ENV = "production"

[http_features]
security_header = true
bot_management = true

CORS Configuration:

const cors = cors({
origin: [
'https://tutor-nexus.com',
'https://staging.tutor-nexus.com',
],
credentials: true,
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
});

A06:2025 - Vulnerable Components

Dependency Scanning:

# Automated via GitHub Actions
pnpm audit --prod

# With severity threshold
pnpm audit --prod --severity=high

CI Security Checks:

CheckToolFrequency
Dependency auditpnpm auditEvery PR
SASTCodeQLEvery PR
Dependency reviewGitHub ActionEvery PR
Secret scanningTruffleHogEvery PR

A07:2025 - Identification Failures

Lucia Auth Configuration:

export function createLucia(db: D1Database) {
const adapter = new D1Adapter(db, {
user: 'users',
session: 'sessions',
});

return new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: !isDev,
sameSite: 'lax',
},
},
getUserAttributes: (attributes) => {
return {
email: attributes.email,
role: attributes.role,
};
},
});
}

OAuth Providers:

// Google OAuth
const googleAuth = new GoogleAuth(clientID, clientSecret, redirectURI);

// GitHub OAuth
const githubAuth = new GitHubAuth(clientID, clientSecret, redirectURI);

A08:2025 - Software/Data Integrity Failures

CI Verification:

  • All commits verified via GitHub Actions
  • Dependency integrity checks (pnpm dedupe --verify)
  • Build reproducibility verification

A09:2025 - Security Logging Failures

Audit Logging:

import { log } from '@tutor-nexus/logger';

log.info('user.login', {
userId: session.userId,
timestamp: new Date().toISOString(),
ip: c.ip,
});

A10:2025 - Unsafe Consumption

Sanitized API Calls:

// Sanitize external input
const sanitizedQuery = sanitize(input);

// Use typed API clients
const courseData = await courseClient.get({
validate: true,
timeout: 5000,
});

API Security

Rate Limiting

import { rateLimit } from '@hono-rate-limiter/cloudflare';

const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
keyGenerator: (c) => {
const session = c.get('session');
return session?.userId ?? c.ip ?? 'anonymous';
},
});

app.use('/api/*', limiter);

CSRF Protection

app.use('/api/*', (c, next) => {
const origin = c.req.header('origin');
const allowedOrigins = ['https://tutor-nexus.com', 'http://localhost:5173'];

if (!origin || !allowedOrigins.includes(origin)) {
return c.text('Forbidden', 403);
}

return next();
});

Secrets Management

Environment Variables

# Never commit secrets to git
.env.example # Template (committed)
.env.local # Developer-specific (gitignored)
.env.production # Secrets (gitignored)

Cloudflare Secrets

# Set production secrets via Wrangler
wrangler secret put AUTH_SECRET
wrangler secret put DATABASE_URL
wrangler secret put OPENAI_API_KEY

Security Checklist

  • Enable 2FA on all accounts
  • Use strong, unique passwords
  • Rotate API keys quarterly
  • Review access logs weekly
  • Update dependencies monthly
  • Run security audits quarterly
  • Penetration test annually
  • Verify PGP key fingerprint before use

Reporting Security Issues

For responsible disclosure of security vulnerabilities, see our SECURITY.md or email [email protected].

PGP Key: See .well-known/pgp-key.txt for the full public key.

See Also