Security Hardening Guide
This skill documents all security patterns implemented in Docklift. Follow these conventions when adding new features to maintain the security posture.
Authentication & Authorization
JWT Tokens
- Signing:
jsonwebtokenwithJWT_SECRETfrom environment (auto-generated on first run if empty). - Expiry: 7 days for session tokens.
- Middleware: All protected routes use
authMiddlewarefromlib/authMiddleware.ts— never manually decode JWTs in route handlers. - Storage: Frontend stores in
localStoragekeydocklift_token. - Download Safety: Backup downloads use
fetch+Authorization: Bearerheader + blob pattern — never put JWTs in URL query parameters.
SSE Tokens (Short-lived)
- SSE connections use dedicated 5-minute tokens (
purpose: 'sse'). - Generated via
POST /api/auth/sse-token. - Passed as query param
?token=<sseToken>(not the long-lived session JWT). - Backend validates
purpose === 'sse'before allowing SSE connections.
Rate Limiting
- All
/api/authroutes are rate-limited viaexpress-rate-limit. - Applied at the mount level in
index.ts:app.use('/api/auth', authLimiter, authRouter).
Password Hashing
- Uses
bcryptwith 12 salt rounds. - Never store or log plaintext passwords.
Route Protection
Public vs Protected Routes (in index.ts)
| Route | Access |
|---|---|
/api/auth/register, /login, /status |
Public (rate limited) |
/api/github/webhook, /callback, /manifest/callback, /setup |
Public (GitHub flow) |
/api/backup/restore-upload with valid setup token |
Public (one-time restore) |
All other /api/* routes |
Requires JWT via authMiddleware |
Internal API Secret
X-Internal-Secretheader used for backend-to-backend calls (e.g., webhook → deploy).- Stored in
INTERNAL_API_SECRETenv var.
Command Execution Security
spawnSync over execSync
RULE: Never use execSync() with string concatenation. Always use spawnSync() with argument arrays to prevent command injection.
// ✅ CORRECT — argument array, no shell injection possible
import { spawnSync } from 'child_process';
spawnSync('docker', ['rm', '-f', containerName], { stdio: 'ignore' });
// ❌ WRONG — string interpolation allows injection
execSync(`docker rm -f ${containerName}`);
Applied in: deployments.ts (container migration).
Error Handling
Error Message Sanitization
RULE: Never expose error.message in API responses. Always return generic messages.
// ✅ CORRECT
catch (error: any) {
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
// ❌ WRONG — leaks internal details
catch (error: any) {
res.status(500).json({ error: error.message });
}
Currently enforced in: auth.ts. Remaining: system.ts /version endpoint (low risk).
Streaming Safety
Disconnection Guard Pattern
All streaming endpoints must use the writeLog guard to prevent crashes on client disconnect:
const writeLog = (text: string) => {
try { if (!res.writableEnded) res.write(text); } catch {}
logs.push(text);
};
Applied in: all 4 streaming handlers in deployments.ts (deploy, stop, restart, redeploy).
Also in docker.ts streamContainerLogs: uses safeWrite() + closed flag + res.on('close') cleanup.
Webhook Security
GitHub Webhook Signature Verification
- Uses
crypto.timingSafeEqual(prevents timing attacks). - Signature verified against
github_webhook_secretstored in DB. - Verification order: Signature is verified FIRST — before any database queries or processing. This prevents unauthenticated requests from triggering DB lookups.
- Raw body (
req.rawBody) is captured viaexpress.json({ verify })callback for accurate HMAC comparison. - Debounced via
recentDeploysMap with 10-second cooldown per project.
Git Token Security
Just-in-Time Token Pattern
GitHub installation tokens are set just-in-time and immediately scrubbed after use:
let gitTokenSet = false;
try {
// Set token in git remote URL
await gitInstance.remote(['set-url', 'origin', authenticatedUrl]);
gitTokenSet = true;
// Pull code
await pullRepo(projectPath, ...);
} finally {
// SECURITY: Always scrub token from remote URL
if (gitTokenSet && gitInstance) {
await gitInstance.remote(['set-url', 'origin', cleanUrl]);
}
}
Applied in: deployments.ts (deploy handler).
Terminal Security
WebSocket Authentication
- JWT: Required to establish WebSocket connection (query param
?token=). - Password Re-verification: After WS connect, user must enter account password.
- Rate Limiting: Max 5 logins/minute.
- Session Limits: Max 3 concurrent connections per user.
- Idle Timeout: Auto-disconnect after 15 minutes of inactivity.
Resize Input Validation
Terminal resize messages are validated to prevent injection:
if (!Number.isInteger(cols) || !Number.isInteger(rows) ||
cols < 1 || cols > 500 || rows < 1 || rows > 200) {
return; // silently ignore invalid resize
}
Applied in: terminal.ts.
Path Security
Path Traversal Prevention (files.ts)
const resolved = path.resolve(projectDir, relativePath);
if (!resolved.startsWith(projectDir)) {
return res.status(403).json({ error: 'Access denied: path traversal detected' });
}
Symlink Protection
const realPath = fs.realpathSync(resolved);
if (!realPath.startsWith(projectDir)) {
return res.status(403).json({ error: 'Access denied: symlink escape' });
}
Project ID Validation
const projectIdRegex = /^[a-f0-9-]{36}$/;
File Upload Safety
Multer Configuration
- Upload destination uses absolute path:
path.join(config.dataPath, 'uploads'). - Temp files are always cleaned up via
try/finally:
try {
// extract zip...
} finally {
try { fs.unlinkSync(req.file.path); } catch {}
}
Infrastructure Security
Security Headers
- Applied globally via
helmet()middleware inindex.ts.
CORS
- Configured from
CORS_ORIGINenvironment variable.
Setup Token (Backup Restore)
- One-time token stored in
.setup-tokenfile. - Consumed (deleted) after single use.
- Only used for unauthenticated
/restore-uploadon fresh installs. - Frontend (
setup/page.tsx) fetches token viaGET /api/auth/setup-tokenand sends asx-setup-tokenheader.
Graceful Shutdown
Backend handles SIGTERM/SIGINT for clean exit:
const shutdown = async (signal: string) => {
server.close(); // Stop accepting new connections
cleanupAllSessions(); // Kill all terminal PTY sessions
await prisma.$disconnect(); // Close database connection
process.exit(0);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Applied in: index.ts.
Checklist for New Features
When adding new endpoints or features, verify:
- Route is protected by
authMiddleware(or has explicit reason to be public) - Error responses use generic messages, not
error.message - Streaming endpoints use disconnection guards
- File paths are validated against traversal and symlink escapes
- Temp files are cleaned up in
finallyblocks - Destructive operations include audit logging (
console.log(\[AUDIT]...`)`) - Shell commands use
spawnSync()with argument arrays, neverexecSync()with strings - Sensitive tokens (JWT, Git) are never placed in URLs — use Authorization headers
- Terminal/WebSocket inputs are validated (type, bounds) before processing
Converted and distributed by TomeVault — claim your Tome and manage your conversions.