Security¶
Findings¶
F-1: Hardcoded session secret (HIGH)¶
server.js:11:
The literal string 3eB(2:\srlI+qa5 is the cookie-signing key for every deployment — dev, uat, prod. This means:
- Anyone who reads the repo (and the repo is in the company's GitHub org — many engineers, vendors, contractors have access) can forge session cookies for any environment.
- The secret cannot be rotated without a code change.
- Even though the handlers currently don't use
req.session, the session cookie is still issued to every client; it could be replayed.
Fix: read from process.env.SESSION_SECRET; throw at startup if unset. Bump the version of the cookie name (e.g., from connect.sid → someli.admin.sid) so old sessions are invalidated.
F-2: Hardcoded Slack bot token (HIGH)¶
routes/auth.js:20-21:
const token = 'xoxb-3144030948916-5918653901701-lKq16vALrV1AqVuTjs9lYNkZ'
var channel = 'C05TS9AHBH6'
A live Slack bot token is hardcoded in source. Anyone with repo read access can post to that Slack channel — or worse, depending on the bot's scopes, perform other workspace actions.
Fix (immediate): rotate the Slack token now. Move to process.env.SLACK_BOT_TOKEN.
F-3: In-memory token revocation (MEDIUM)¶
middlewares/auth.js calls revokedTokens.has(token) where revokedTokens is a Set. The set is in-process memory only. Implication:
- Process restart → all revoked tokens become valid again
- Multi-replica deploy → revocation on replica A doesn't apply on replica B
- Since admin tokens have no expiry encoded in them (per
authentication.md), a stolen token is effectively long-lived
Fix: persist revocation in a DB table (tRevokedToken(token, revoked_at, expires_at)) and check it. Or, encode an expiry in the encrypted token ({ ..., exp: <ms> }) and use that.
F-4: Token has no expiry (MEDIUM)¶
The encrypted-token format produced by helper/tokenGenerator.js (verify) does not include an expiration claim. The middlewares/auth.js decryption succeeds as long as the AES key and ciphertext match, regardless of age.
Fix: include { ..., issuedAt, expiresAt } in the encrypted payload; validate Date.now() < expiresAt after decryption.
F-5: CORS wildcard (MEDIUM)¶
server.js does app.use(cors()) with no options — that means Access-Control-Allow-Origin: *. Combined with token-in-header authentication, the wildcard allows any origin's JavaScript to call the API as long as it knows a valid token. This is a defence-in-depth issue rather than an exploit on its own, but the admin API should restrict origins to known FE hosts (e.g., https://admin.someli.ai).
Fix: cors({ origin: ['https://admin.someli.ai', 'http://localhost:8080'], credentials: true }).
F-6: Webhook body-parser exemption with no handlers (LOW)¶
server.js exempts /stripe_webhooks, /paddle_sandbox_webhooks, /paddle_production_webhooks from JSON parsing, but no handler exists for those paths. Any request to them will 404. Not exploitable, but a misleading signal — verify whether the exemption is needed.
F-7: Role gating done in SQL only (MEDIUM)¶
There is no requireRole(...) middleware. Each endpoint enforces role by SQL WHERE filters. A new endpoint added by a future engineer who forgets to filter is a privilege-escalation. Examples to audit: routes/auth.js's getAllOwnerList filters by role == 'test' if and only if the query string sets role=test. If role is omitted, it returns everything (which may be correct for admins but should be explicit).
Fix: introduce requireRole(['SUPER_ADMIN', 'ADMIN', 'ACCOUNT_MANAGER']) middleware. Apply per-router.
F-8: SQL injection via unparameterised dynamic filters (MEDIUM — needs spot check)¶
Searching for ${ and con.query(\SELECTpatterns will find places where SQL is built by string concatenation. Example pattern observed inroutes/auth.js:181`:
The usersFilter value here is hardcoded → safe. But the same handler also does:
However, audit recommends grep -nE "con\.query\(\SELECT.\\$\{" routes/.jsto find places where the interpolated value comes fromreq.query/req.body. Several similar handlers exist insomeli-api` with this anti-pattern.
Fix: replace string interpolation with ? placeholders and array-bound params.
F-9: 150 MB request body limit (LOW)¶
server.js:
A 150 MB JSON body is large enough to be a DoS vector if any unauthenticated endpoint accepts JSON. Currently /authenticate and /webauthenticate are unauthenticated; both accept JSON. A flood of large bogus bodies will exhaust memory.
Fix: tighten to e.g., 1 MB for unauthenticated endpoints; reserve the high limit for endpoints that need it (file uploads — though those use express-fileupload, not JSON).
F-10: conf/credentials.json may be committed (HIGH if true)¶
A file conf/credentials.json is present in the working tree. If this is a Google service-account JSON (which the path suggests), and if it's actually committed (rather than gitignored), it is a leaked credential. Verify with git -C ... log --all -- conf/credentials.json and git -C ... ls-files conf/credentials.json.
Fix: rotate the service-account credentials; remove from git history; add to .gitignore.
Findings out of scope (cross-component)¶
- Token format shared with
someli-api:tokenGenerator.jsis byte-identical. A weakness here (e.g., predictable AES key derivation) affects both repos. Audit the underlying crypto in../someli-api/security.md. - Role taxonomy mismatch risk: the FE pins role IDs as env vars (
SUPER_ADMIN=1, etc.); the BE uses SQLrole_typeintegers; drift between the two means an FE that disables a button doesn't prevent the BE from honouring the call. Same finding applies toadmin_console_R.
What's not a finding here¶
- Most route handlers parameterise their queries via
mysql2's?placeholders. SQL injection is restricted to the dynamic-filter patterns called out in F-8. - Password hashing uses
bcryptjs— fine (slow but secure). - File uploads via
express-fileuploaduse temp files; no path traversal observed in spot-checks. - HTTPS / TLS is terminated upstream by nginx — not this repo's concern.