Skip to content

Security

Findings

F-1: Hardcoded session secret (HIGH)

server.js:11:

app.use(expressSession({
    secret: "3eB(2:\\srlI+qa5",
    resave: false,
    saveUninitialized: true
}))

The literal string 3eB(2:\srlI+qa5 is the cookie-signing key for every deployment — dev, uat, prod. This means:

  1. 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.
  2. The secret cannot be rotated without a code change.
  3. 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.sidsomeli.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`:

usersFilter = `AND m.role_type = 11`;
// later: con.query(`SELECT ... WHERE ... ${usersFilter}`)

The usersFilter value here is hardcoded → safe. But the same handler also does:

if (role == 'all') usersFilter = '';
else if (role == 'test') usersFilter = `AND m.role_type = 11`;
which is safe.

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:

express.json({ limit: '150mb', extended: true, parameterLimit: 50000 })

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.js is 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 SQL role_type integers; drift between the two means an FE that disables a button doesn't prevent the BE from honouring the call. Same finding applies to admin_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-fileupload use temp files; no path traversal observed in spot-checks.
  • HTTPS / TLS is terminated upstream by nginx — not this repo's concern.