Security Audit
Vulnerabilities found during the code review, ranked by severity.
Summary
| Severity | Count |
|---|---|
| Critical | 2 |
| High | 1 |
| Medium | 5 |
| Moderate | 5 |
JWT Tokens Decoded But Never Verified
packages/openmembers/routes/login.js — lines 167, 405, 440
The code uses jwt.decode() instead of jwt.verify(). This means JWT signatures are never validated — anyone can forge a token and authenticate as any user.
// CURRENT (insecure) - no signature verification
const decoded = jwt.decode(token, config.shared_secret);
req.session.user = decoded.user;
req.session.apikey = decoded.apikey;
// FIX - verify the signature
const decoded = jwt.verify(token, config.shared_secret);
req.session.user = decoded.user;
req.session.apikey = decoded.apikey;
Impact: Complete authentication bypass. An attacker can craft a JWT with any user ID and apikey to gain unauthorized access.
Deprecated Crypto Functions
Multiple files (see table below)
Multiple files use crypto.createCipher() and crypto.createDecipher(), deprecated since Node 10. These use a weak key derivation method vulnerable to known attacks.
| File | Function Used |
|---|---|
packages/openmembers/routes/login.js | createCipher() |
packages/openmembers/libs/pin.js | createCipher() / createDecipher() |
packages/openmembers/libs/mail.js | createDecipher() |
packages/openmembers/libs/hbshelpers.js | createDecipher() |
packages/openmembers/routes/admin/helpers.js | Both (mixed migration) |
packages/api/models/user_model.js | createDecipher() |
// CURRENT (deprecated, weak)
var cipher = crypto.createCipher("aes-128-cbc", key);
// FIX - use IV-based cipher with proper key derivation
const key = crypto.scryptSync(config.secret, salt, 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
Plaintext Password Stored in Encrypted Session
packages/openmembers/routes/login.js — lines 93-98
The user's plaintext password is encrypted (not hashed) and stored in the session. If the encryption key leaks, all passwords are immediately exposed.
// CURRENT - reversible encryption of plaintext password
req.session.encrypted = encrypt(
JSON.stringify({
email: req.body.email,
password: req.body.password // plaintext!
})
);
Impact: Passwords should be hashed (bcrypt/argon2), never stored reversibly. Compromising the encryption key exposes every stored password.
Path Traversal in Image Handler
packages/openmembers/routes/image.js — lines 28-34
The image handler decodes filenames with decodeURIComponent() but only checks if the path starts with /. It does not block ../ sequences, allowing an attacker to read arbitrary files.
// CURRENT - only checks first character
const decoded_filename = decodeURIComponent(filename).replace("/uploads/", "");
if (decoded_filename.indexOf("/") == 0) {
return path.join(process.cwd(), "/public", decoded_filename);
}
return path.join(upload_dir, decoded_filename);
// FIX - validate resolved path stays within allowed directory
const resolvedPath = path.resolve(upload_dir, decoded_filename);
if (!resolvedPath.startsWith(path.resolve(upload_dir))) {
throw new Error("Path traversal attempt detected");
}
Command Injection in Papercut Module
packages/openmembers/libs/papercut.js — lines 120-130
Usernames and parameters are concatenated directly into shell commands without escaping. An attacker controlling a username (e.g. via API) could inject shell commands.
// CURRENT - no shell escaping
papercut.serverCmdFormat = function(cmd, username, params) {
const parts = ["/home/papercut/server/bin/linux-x64/server-command", cmd];
if (username) parts.push(username); // unescaped!
if (params) params.forEach(param => {
parts.push('"' + param + '"'); // minimal quoting only
});
return parts.join(" ");
};
Fix: Use shell-escape or pass arguments as an array to avoid shell interpretation.
Session Cookies Missing Security Flags
packages/openmembers/app.js — lines 37-41
Session configuration is missing critical security flags.
// CURRENT
app.use(session({
secret: config.secret,
store: new RedisStore({}),
resave: false,
}));
// FIX
app.use(session({
secret: config.secret,
store: new RedisStore({}),
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // no JS access
sameSite: 'lax', // CSRF protection
maxAge: 86400000 // 24h expiry
}
}));
TLS Certificate Validation Disabled
config/sample.json — lines 17-18
"rejectUnauthorized": false disables TLS certificate validation, making all HTTPS connections vulnerable to man-in-the-middle attacks.
Fix: Remove this setting or set to true in production. If needed for dev, use environment-specific config.
Hardcoded Secret in Sample Config
config/sample.json — line 143
The sample configuration includes "secret": "$FySuO85$DxB". Developers may copy this directly to production. Sample configs should use obvious placeholder values.
Moderate Issues
Weak Brute Force Protection
packages/openmembers/routes/login.js — lines 33-37
Only 1-second wait after 5 failed login attempts. Should use exponential backoff (e.g. 1s, 2s, 4s, 8s...) and consider account lockout after repeated failures.
SSH Private Key in Config Files
config/sample.json, packages/openmembers/libs/papercut.js
SSH private keys for Papercut are referenced from config files on disk. Should use environment variables or a secrets manager (Vault, AWS Secrets Manager).
EOL Docker Base Images
node:carbon (Node 8, EOL Dec 2019) and node:16 (EOL Sept 2023) contain known unpatched vulnerabilities. Containers also run as root.
No Rate Limiting Verification on API
Throttle configuration exists in config (burst: 100, rate: 50) but implementation wasn't confirmed in the API server code. If not enforced, the API is vulnerable to denial-of-service.
Cron Scheduler Uses eval()
packages/cron/cron.js
Schedule records store JavaScript code strings in the action field, which are executed via eval(). If the database is compromised, arbitrary code execution is possible.
Priority Remediation Plan
| Priority | Action | Effort |
|---|---|---|
| Immediate | Replace jwt.decode() with jwt.verify() | Low |
| Immediate | Migrate all deprecated crypto functions | Medium |
| This Week | Remove plaintext password storage from sessions | Medium |
| This Sprint | Fix path traversal in image handler | Low |
| This Sprint | Add session cookie security flags | Low |
| This Sprint | Escape shell parameters in Papercut | Low |
| This Sprint | Enable TLS validation in production | Low |
| Next Quarter | Update Docker images to Node 20+ | Medium |
| Next Quarter | Implement secrets management | High |
| Next Quarter | Replace eval() in cron scheduler | Medium |