Existing Platform Fixes
Prioritised fixes for the live WorkSpaceMan API at api.workshop17.co.za. These secure the system while the AI-powered Proximity Green platform is built. Informed by the live API analysis of all 88 models.
Context: Live API Exposure
The API at api.workshop17.co.za runs JXP v2.0.0 with 88 auto-generated REST endpoints. Every model — including sensitive ones like user, invoice, ledger, apikey, and payment — has full CRUD endpoints exposed. The security issues found in the code review are actively exploitable against this live API.
/api/{model} for all 88 models. Combined with unverified JWT tokens and no input validation, an attacker with a forged token could read, modify, or delete data across the entire system — including financial records, user credentials, and payment tokens.
| Sprint | Focus | Effort | Timeline |
|---|---|---|---|
| Sprint 1 | Critical — auth bypass & data exposure | ~16 hours | This week |
| Sprint 2 | High — injection, traversal, session hardening | ~16 hours | Next 2 weeks |
| Sprint 3 | Moderate — infrastructure, monitoring, hardening | ~20 hours | Next month |
Total: ~52 hours / R41,600 at R800/hr. Claude Code assists with all fixes.
Sprint 1: Critical Auth & Data Exposure
Fix 1.1: Replace jwt.decode() with jwt.verify()
Problem: JWT tokens are decoded without signature verification. Anyone can forge a token and authenticate as any user.
api.workshop17.co.za — including /api/user, /api/invoice, /api/payment, /api/ledger, and /api/apikey. An attacker could extract all API keys, financial data, and user credentials.
- Open
packages/openmembers/routes/login.js - Find all
jwt.decode(token, config.shared_secret)(lines ~167, 405, 440) - Replace each with
jwt.verify(token, config.shared_secret)inside try/catch:try { const decoded = jwt.verify(token, config.shared_secret); req.session.user = decoded.user; req.session.apikey = decoded.apikey; } catch (err) { return res.status(401).send("Invalid or expired token"); } - Test: valid token works, modified token returns 401
- Deploy to staging, verify login flow, deploy to production
Fix 1.2: Restrict sensitive API model access
Problem: The JXP framework auto-exposes CRUD endpoints for ALL 88 models. Several models should never be publicly accessible, even to authenticated users.
apikey (leaks all API keys), config (leaks system secrets), instancesettings (leaks integration credentials), schedule (contains eval'd code), token, refreshtoken, handovertoken (leaks auth tokens), payfasttoken (leaks payment credentials).
- Add middleware to block public access to sensitive models. In the API server setup:
const RESTRICTED_MODELS = [ 'apikey', 'config', 'instancesettings', 'schedule', 'token', 'refreshtoken', 'handovertoken', 'payfasttoken' ]; RESTRICTED_MODELS.forEach(model => { app.all(`/api/${model}*`, (req, res) => { if (!req.user || !req.user.admin) { return res.status(403).json({ error: 'Access denied' }); } next(); }); }); - Verify that the
/docs/model/pages don't expose sensitive schema details to unauthenticated users - Add admin-only middleware to
/aggregateand/queryroutes (these accept raw MongoDB queries and can bypass filters) - Test: unauthenticated request to
/api/apikeyreturns 403
Fix 1.3: Migrate deprecated crypto functions
Problem: crypto.createCipher()/createDecipher() use weak key derivation. Encrypted PINs, session tokens, and user data stored via the user and pin models are vulnerable.
user (pin, wifi_password fields), pin model. If an attacker reads encrypted values via GET /api/user, the weak encryption can be broken offline.
- Create shared
packages/openmembers/libs/crypto-util.js:const crypto = require('crypto'); const ALGO = 'aes-256-cbc'; function deriveKey(secret) { return crypto.scryptSync(secret, 'proximity-green-salt', 32); } function encrypt(text, secret) { const key = deriveKey(secret); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(ALGO, key, iv); let enc = cipher.update(text, 'utf8', 'hex'); enc += cipher.final('hex'); return iv.toString('hex') + ':' + enc; } function decrypt(text, secret) { const key = deriveKey(secret); const [ivHex, encHex] = text.split(':'); const decipher = crypto.createDecipheriv(ALGO, key, Buffer.from(ivHex, 'hex')); let dec = decipher.update(encHex, 'hex', 'utf8'); dec += decipher.final('utf8'); return dec; } function decryptLegacy(text, secret) { const d = crypto.createDecipher('aes-128-cbc', secret); let dec = d.update(text, 'hex', 'utf8'); dec += d.final('utf8'); return dec; } module.exports = { encrypt, decrypt, decryptLegacy }; - Update all 6 files:
routes/login.js,libs/pin.js,libs/mail.js,libs/hbshelpers.js,routes/admin/helpers.js,models/user_model.js - Write migration script to re-encrypt all stored PINs
- Run on staging first, verify decrypt works, then run on production in low-traffic window
Fix 1.4: Stop storing plaintext passwords in sessions
Problem: Plaintext passwords encrypted reversibly in the session. If the encryption key leaks (see Fix 1.3), all passwords are exposed.
- Store only the API key in the session — not the password:
// BEFORE (insecure) req.session.encrypted = encrypt(JSON.stringify({ email: req.body.email, password: req.body.password })); // AFTER req.session.apikey = loginResult.apikey; req.session.user = loginResult.user; - Update all code paths reading
req.session.encrypted - Update apihelper to use stored API key for authentication
- After deploy, flush Redis session store to force re-login:
redis-cli FLUSHDB
Sprint 2: Injection, Traversal & Session Hardening
Fix 2.1: Patch path traversal in image handler
Problem: decodeURIComponent(filename) allows ../ traversal to read arbitrary server files including config with secrets.
- Replace the
fnamefunction with path validation:const fname = (filename, upload_dir) => { const decoded = decodeURIComponent(filename) .replace("/uploads/", ""); const base = decoded.startsWith("/") ? path.join(process.cwd(), "public") : upload_dir; const resolved = path.resolve(base, decoded); if (!resolved.startsWith(path.resolve(base))) { throw new Error("Invalid file path"); } return resolved; }; - Wrap handler in try/catch, return 400 for invalid paths
- Test:
/image/..%2F..%2Fetc%2Fpasswdmust return 400
Fix 2.2: Escape shell parameters in Papercut
Problem: Usernames concatenated into shell commands. A user record with a malicious papercut_username could trigger command injection on the Papercut server.
PUT /api/user/{id} allows setting papercut_username, an attacker could inject via the API. Validate this field at the model level.
- Install
shell-escape:yarn add shell-escape - Replace
serverCmdFormatto useshellescape(parts)instead ofparts.join(" ") - Add username format validation:
/^[a-zA-Z0-9._@-]+$/ - Add the same validation to the
usermodel'spapercut_usernamefield in the Mongoose schema
Fix 2.3: Add session cookie security flags
Problem: Session cookies missing secure, httpOnly, sameSite flags.
- Update session config:
app.use(session({ secret: config.secret, store: new RedisStore({}), resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 } })); - Set
app.set('trust proxy', 1)for Nginx
Fix 2.4: Enable TLS certificate validation
Problem: "rejectUnauthorized": false disables TLS validation on all outbound HTTPS to Xero, PayFast, BulkSMS, etc.
- Set
"rejectUnauthorized": truein production config (or remove thetlssection entirely) - Remove any
NODE_TLS_REJECT_UNAUTHORIZED = '0' - Test all outbound integrations on staging: Xero token refresh, PayFast payments, BulkSMS
- If a specific internal service uses self-signed certs, scope the exception to only that connection
Fix 2.5: Strengthen brute force & add API rate limiting
Problem: 1-second wait after 5 failed logins. No rate limiting on the API layer.
/query and /aggregate endpoints accept raw MongoDB queries. Without rate limiting, an attacker could run expensive aggregation pipelines to DoS the database, or enumerate data via repeated filtered queries.
- Update express-brute: 3 retries, 5-second min wait, 15-minute max, 1-hour window
- Add
express-rate-limitto the API server: 100 requests/15 min per IP - Add stricter limits on
/queryand/aggregateroutes: 20 requests/15 min - Log all blocked requests with IP and target endpoint for monitoring
Sprint 3: Infrastructure & Hardening
Fix 3.1: Update Docker images to Node 20
Problem: node:carbon (Node 8, EOL 2019) and node:16 (EOL 2023) have known CVEs. Containers run as root.
- Update both Dockerfiles to
node:20-slim - Add non-root user:
RUN groupadd -r app && useradd -r -g app appandUSER app - Add
HEALTHCHECKdirective - Test all services on staging for Node 20 compatibility
- If OpenSSL issues, add
NODE_OPTIONS=--openssl-legacy-provideras temporary bridge
Fix 3.2: Sanitise config & secrets management
Problem: Hardcoded secrets in sample config, SSH keys referenced from config files.
- Replace all secrets in
config/sample.jsonwithCHANGE_ME_*placeholders - Add startup validation that rejects placeholder values
- Move SSH key paths to environment variables in systemd service files
- Ensure key files have
chmod 600 - Verify production config is in
.gitignore - Rotate production secrets if they match sample values
Fix 3.3: Add input validation to critical API routes
Problem: No input validation. req.body goes directly to MongoDB. The POST /api/{model} and PUT /api/{model}/{id} endpoints accept arbitrary JSON.
POST /api/user, PUT /api/invoice, etc., an attacker could inject unexpected fields including admin: true on user records or manipulate amount fields on financial models without validation.
- Install
express-validator:yarn add express-validator - Add validation to the 5 highest-risk OpenMembers routes: login, organisation update, booking create, lead create, user update
- For the JXP API layer, add a global middleware that strips dangerous fields from POST/PUT:
const PROTECTED_FIELDS = ['admin', '_owner_id', '_deleted', 'password', 'temp_hash', 'apikey']; app.use('/api', (req, res, next) => { if (['POST', 'PUT'].includes(req.method) && req.body) { PROTECTED_FIELDS.forEach(f => delete req.body[f]); } next(); }); - Allow
adminandpasswordfields only from requests with super_user role
Fix 3.4: Disable docs endpoint in production
Problem: The /docs/model/{name} endpoints expose full schema details for all 88 models including field types, indexes, and permissions. This is an information disclosure risk in production.
- Add environment check to disable docs in production:
if (process.env.NODE_ENV === 'production') { app.all('/docs/*', (req, res) => { res.status(404).send('Not found'); }); } - Alternatively, restrict docs to authenticated admin users only
- The docs at
api.workshop17.co.zaremain useful for internal reference — restrict by IP or auth, don't remove entirely
Fix 3.5: Add security monitoring
Problem: Sentry is configured for error tracking but there's no security-specific monitoring. Failed auth attempts, rate limit hits, and suspicious queries go unnoticed.
- Create a security logger middleware:
function securityLog(event, req, details = {}) { console.warn(JSON.stringify({ event, ip: req.ip, user: req.user?._id, path: req.path, method: req.method, timestamp: new Date().toISOString(), ...details })); } - Log on: failed auth, rate limit hit, restricted model access, path traversal attempt, protected field injection
- Set up Sentry alerts for security event patterns (e.g. >10 failed auths from same IP in 5 min)
- Review logs weekly until the new platform launches
Summary
| # | Fix | Severity | Effort | Sprint |
|---|---|---|---|---|
| 1.1 | jwt.decode() → jwt.verify() | Critical | 1 hr | 1 |
| 1.2 | Restrict sensitive model endpoints | Critical | 4 hrs | 1 |
| 1.3 | Migrate deprecated crypto | Critical | 8 hrs | 1 |
| 1.4 | Remove password from session | High | 3 hrs | 1 |
| 2.1 | Patch path traversal | Medium | 1 hr | 2 |
| 2.2 | Escape Papercut shell params | Medium | 2 hrs | 2 |
| 2.3 | Session cookie security flags | Medium | 1 hr | 2 |
| 2.4 | Enable TLS validation | Medium | 2 hrs | 2 |
| 2.5 | Brute force + API rate limiting | Medium | 4 hrs | 2 |
| 3.1 | Docker images to Node 20 | Moderate | 6 hrs | 3 |
| 3.2 | Config & secrets cleanup | Moderate | 2 hrs | 3 |
| 3.3 | Input validation on critical routes | Moderate | 7 hrs | 3 |
| 3.4 | Disable docs in production | Moderate | 1 hr | 3 |
| 3.5 | Security monitoring | Moderate | 4 hrs | 3 |
| Item | Amount |
|---|---|
| Developer time (52 hrs @ R800/hr) | R41,600 |
| Claude Code Max (1 month) | R1,850 |
| Total | R43,450 |