Summary

SeverityCount
Critical2
High1
Medium5
Moderate5
Critical

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.

Critical

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.

FileFunction Used
packages/openmembers/routes/login.jscreateCipher()
packages/openmembers/libs/pin.jscreateCipher() / createDecipher()
packages/openmembers/libs/mail.jscreateDecipher()
packages/openmembers/libs/hbshelpers.jscreateDecipher()
packages/openmembers/routes/admin/helpers.jsBoth (mixed migration)
packages/api/models/user_model.jscreateDecipher()
// 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);
High

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.

Medium

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");
}
Medium

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.

Medium

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
    }
}));
Medium

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.

Medium

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

Moderate

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.

Moderate

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).

Moderate

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.

Moderate

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.

Moderate

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

PriorityActionEffort
ImmediateReplace jwt.decode() with jwt.verify()Low
ImmediateMigrate all deprecated crypto functionsMedium
This WeekRemove plaintext password storage from sessionsMedium
This SprintFix path traversal in image handlerLow
This SprintAdd session cookie security flagsLow
This SprintEscape shell parameters in PapercutLow
This SprintEnable TLS validation in productionLow
Next QuarterUpdate Docker images to Node 20+Medium
Next QuarterImplement secrets managementHigh
Next QuarterReplace eval() in cron schedulerMedium