Skip to content

Someli-admin-api — code inspection

Audit date: 2026-05-17. Lens: concrete code-level issues not already covered by someli-doc/audit/Someli-admin-api/security.md and friends.

Summary

Twenty-one findings across six severity bands. The most serious are: a plaintext-password authentication bypass in /webauthenticate that entirely defeats bcrypt; a missing return after failed token validation in tempLogin that lets an invalid token authenticate; a committed Google OAuth client_secret in conf/credentials.json; and SQL injection via direct interpolation of user-supplied search strings into query strings across at least eight endpoints. Three groups of routes in routes/routes.js have no auth middleware at all (they register on the raw Express app rather than the auth-protected router). Several responses leak hashed passwords in their JSON payloads. Recurring themes: the auth middleware boundary is inconsistently applied, req.body inputs are interpolated directly into SQL, and debug console.log calls that dump credentials are live in production paths.

Critical

C1. Plaintext password fallback in /webauthenticate bypasses bcrypt entirely

  • File: routes/routes.js:272-280
  • What: After bcrypt.compare fails (checkUser !== true), the code falls through to else if (reqObj.password == result[0].password) — a direct string equality comparison of the submitted plaintext password against whatever is stored in the DB column.
  • Why it matters: Any account that was created or migrated with a plaintext password stored in tMember.password can be logged in without knowing the bcrypt hash. Additionally, because the bcrypt callback calls res.send for both success and failure but does not return after the role-check guard at line 255 (it sends a 403 and falls through), there are race-condition-style paths where two res.send calls can fire. The plaintext comparison branch is the most direct exploit: send password: <plaintext> and authenticate if any legacy account has a plaintext password.
  • Fix: Remove lines 272-280 entirely. There is no legitimate reason to accept a plaintext password after bcrypt comparison fails.

C2. Missing return after failed-token guard in tempLogin branch

  • File: routes/routes.js:307-321
  • What: After decryptData(reqObj.token) returns falsy (verified is null/undefined), the code sends a 403 response but does not return. Execution continues to line 314, where verified.userId is accessed (throwing a TypeError: Cannot read properties of null), which is caught by the outer try/catch at line 327, which then sends a 500 response body. The real problem is the missing return, which means if decryptData somehow succeeds but returns an unexpected truthy value, the code blindly proceeds to populate responseObj.status = true and return a valid session.
  • Why it matters: The correct guard is never enforced. Any token-decode error should stop execution.
  • Fix: Add return before res.send(responseObj) on line 312, so the branch exits after sending 403:
    if (!verified) {
      responseObj.code = 403;
      responseObj.message = 'Invalid Token';
      return res.send(responseObj);
    }
    

C3. Committed Google OAuth client_secret in conf/credentials.json

  • File: conf/credentials.json:8
  • What: "client_secret": "drGPpS_h7PCgekNpN5P3Fr5c" — a live Google OAuth 2.0 client secret for project someli-web (client ID 305153224854-9h95b...) is checked into the repository.
  • Why it matters: Anyone with read access to the repo (engineers, contractors, GitHub org members) can use this credential to impersonate the OAuth application, perform token exchanges, or forge OAuth flows against any user who has authorized this app.
  • Fix (immediate): Rotate the OAuth client secret in Google Cloud Console. Add conf/credentials.json to .gitignore. Remove the file from git history (git filter-repo or BFG). Reference the secret only via process.env.GOOGLE_CLIENT_SECRET.

C4. SQL injection via unescaped user input in multiple search endpoints

  • File: routes/routes.js:1278, routes/routes.js:1322, routes/routes.js:1332, routes/routes.js:1377, routes/routes.js:681, routes/auth.js:120-121, routes/auth.js:595, routes/auth.js:677
  • What: User-supplied strings from req.body.search, req.query.status, req.query.plan, req.params.searchQuery, and req.body.searchUser are interpolated directly into SQL LIKE clauses and WHERE conditions with no escaping or parameterisation.
  • routes/routes.js:1278: `...name like '${reqObj.search}%'...`reqObj.search from req.body
  • routes/routes.js:1322: `...m.firstName like '%${req.params.searchQuery}%'...` — URL path param
  • routes/auth.js:120-121: status and plan from req.query inserted into WHERE clauses
  • routes/auth.js:595: search from req.body into multiple LIKE predicates
  • routes/routes.js:681: req.body.searchUser into a LIKE clause via sync-mysql
  • Why it matters: These are authenticated endpoints, but any compromised admin-level token gives an attacker full SQL read/write access to the DB. The sync-mysql driver does not automatically escape values; neither does the queryAll wrapper used in most paths. A payload like '; DROP TABLE tMember; -- would execute.
  • Fix: Replace all string interpolations with parameterised queries. For sync-mysql use the ? placeholder syntax it supports. For actions.queryAll, pass a params array alongside q and call db.query(q, params, cb).

High

H1. Entire routes.js route set has no auth middleware — all endpoints publicly accessible

  • File: routes/routes.js:122-1397, server.js:80-82
  • What: routes/auth.js correctly applies router.use(auth) at line 72, gating every route in that file behind the middlewares/auth.js token check. But routes/routes.js registers all its routes (/authenticate, /webauthenticate, /me, /addOrUpdatePersonnel, /getAllOwnerList, /getPersonnel/:pId, /updateProfile, /NewAccManager, /deleteAccount, /deleteUser, /addOrUpdateAffiliateDetails, /getAffiliateMarketingURL/:pId, /searchAffiliatePeople, etc.) directly on App.server (the raw Express app) via apiRoutes.prototype.init. There is no router.use(auth) call anywhere in routes.js. The single-route guard methods.ensureToken only covers GET / (line 127). Every other route in routes.js is unauthenticated.
  • Why it matters: POST /deleteUser, POST /deleteAccount, POST /updateProfile, POST /NewAccManager (elevates any user to account-manager role), and POST /addOrUpdateAffiliateDetails are completely open to anonymous callers. An unauthenticated attacker can delete any user account or escalate any member's role.
  • Fix: Introduce const authMiddleware = require('./middlewares/auth') at the top of routes.js and apply it to every route that is not the login endpoints. The simplest approach is to use a sub-router for protected routes with router.use(authMiddleware) then register the sub-router on the app.

H2. POST /webauthenticate logs the full DB result row including hashed password to stdout

  • File: routes/routes.js:254
  • What: console.log("ress", result, checkUser, result?.[0].password, reqObj.password) — this explicitly logs the bcrypt hash from the DB and the plaintext password submitted by the user on every login attempt.
  • Why it matters: Server logs are aggregated to Lightsail, Slack, or other log sinks. Anyone with log access sees every submitted credential and every bcrypt hash. Bcrypt hashes can be cracked offline. Combined with finding C1, this is high impact.
  • Fix: Remove lines 218, 221, and 254 entirely. If authentication-flow logging is needed for debugging, log only sanitised data (e.g., userId, success: true/false).

H3. POST /webauthenticate also authenticates if reqObj.social == true regardless of password

  • File: routes/routes.js:281-289
  • What: A third else if branch in the bcrypt callback grants a session token to any caller who sets reqObj.social = true in the POST body — without any social-provider token verification, OAuth state check, or provider signature.
  • Why it matters: Any client can POST { email: "victim@example.com", sid: "victim-sid", password: "anything", social: true } and receive a valid session token for that account, bypassing password checking entirely. This is exploitable by anyone who can enumerate a valid sid (which is returned in many list endpoints).
  • Fix: Remove lines 281-289. Social login should verify the identity via a provider-issued token, not a client-supplied boolean flag.
  • File: routes/auth.js:256, routes/auth.js:753, routes/auth.js:817, routes/auth.js:897
  • What: The downlineQuery in getAllOwnerList (line 256) and searchAccountUser (line 753) selects m.password and includes it in the response payload. The AccountManagersuserlist (line 817) and searchUser (line 897) queries also SELECT m.password plus a static '12345' AS auth_pass.
  • Why it matters: Every paginated list response returns bcrypt hashes for every user in the result set. Any caller with a valid admin token can harvest all password hashes with a few paginated requests. The auth_pass: '12345' pseudo-column is additionally misleading.
  • Fix: Remove m.password from all SELECT lists in these queries. Remove the '12345' AS auth_pass literal.

H5. POST /analyzeKnowledge passes user-controlled values directly to spawn as CLI arguments

  • File: routes/routes.js:640
  • What: spawn('python', ['Nova-Pro_KnowledgeBased_Content.py', AccountId, languageModelId, topic])AccountId, languageModelId, and topic come directly from req.body with no validation beyond nullity. While Node's spawn with an array does not invoke a shell (so classic ; rm -rf shell injection does not apply), topic is an unbounded string that becomes sys.argv[3] in the Python script, which may process it unsafely. More importantly, the handler has no auth check (see H1) and no length limit on topic.
  • Why it matters: The endpoint is unauthenticated (per H1). An unbounded string in topic sent to a Python subprocess that reads sys.argv[3] can cause OOM or unexpected behaviour depending on how the Python script uses it.
  • Fix: Add auth middleware. Validate AccountId and languageModelId are integers; validate topic is a string with a maximum length (e.g., 500 chars). The Python script should also sanitise its own argv inputs.

H6. pyProg.stderr.on('data') sends a response after pyProg.on('close') already sent one — double response crash

  • File: routes/routes.js:649-674
  • What: The close handler at line 649 calls res.status(200).json(...) or res.status(500).json(...). The stderr.on('data') handler at line 671 also calls res.status(500).json(...). If the Python process writes to stderr and closes, both handlers fire. Express will log "Cannot set headers after they are sent to the client" and the second res.json call throws an uncaught exception in the event handler, crashing the handler context.
  • Why it matters: If the Python script writes any warning to stderr before exiting, the request crashes rather than completing. This can be triggered by any python warning output.
  • Fix: Use a flag let responded = false and check/set it before each res call. Alternatively, collect stderr in a buffer and only evaluate it in close.

Medium

M1. conf/credentials.json is not listed in .gitignore

  • File: .gitignore (file not found in glob), conf/credentials.json
  • What: The file is present in the working tree and was not found to be gitignored. Per the findings in C3, it contains a live secret. Independent of the C3 rotation, the .gitignore must be updated to prevent re-committing.
  • Fix: Add conf/credentials.json (or conf/*.json) to .gitignore.

M2. GET /me builds its SQL WHERE clause via direct interpolation of the Authorization header value

  • File: routes/routes.js:371
  • What: `...AND (m.auth_token = '${reqObj.authorization}' OR a.providerAuth = '${socialAuth}')` — both reqObj.authorization and socialAuth come from req.headers.authorization, a caller-controlled string, interpolated directly into the SQL query.
  • Why it matters: An attacker who can send arbitrary Authorization header values (trivial) can inject SQL into this WHERE clause. This endpoint is unauthenticated (see H1 — no auth middleware on routes.js). Sending Authorization: ' OR 1=1 -- would return all members.
  • Fix: Use a parameterised query: `...AND (m.auth_token = ? OR a.providerAuth = ?)` with [reqObj.authorization, socialAuth] as bound parameters.

M3. POST /authenticate interpolates the email directly into SQL without escaping

  • File: routes/routes.js:152
  • What: conditions += `m.sid='${reqObj.email}' AND m.isDeleted=0`;reqObj.email is not validated or escaped. The same pattern appears at line 231 for the webauthenticate endpoint with reqObj.email and reqObj.sid.
  • Why it matters: The login endpoint is unauthenticated by design. A specially crafted email like ' OR '1'='1 would return all rows, and the code then selects result[0], potentially authenticating as the first user in the table.
  • Fix: Parameterise: `SELECT ... WHERE m.sid = ? AND m.isDeleted = 0` with [reqObj.email].

M4. addOrUpdatePersonnel handler returns immediately without performing any action — dead endpoint

  • File: routes/routes.js:436-443
  • What: The POST /addOrUpdatePersonnel handler checks !req.body and returns 400, but otherwise falls off the end of the function without calling res.send. If req.body is present (normal case), the handler does nothing and the request hangs indefinitely (no response, connection times out on the client).
  • Why it matters: The corresponding UI in admin_console_R almost certainly calls this endpoint. Every call with a valid body hangs. There is also a separate GET /getAllOwnerList handler in routes.js (lines 445-548) that contains the real addOrUpdate logic body, meaning the GET getAllOwnerList handler silently runs upsert logic on tPersonnel on what looks like a read request.
  • Fix: Move the insert/update logic from lines 515-547 into the POST /addOrUpdatePersonnel handler. The GET /getAllOwnerList handler should be a pure read.

M5. GET /getUserdetail:userid does not respond at all when the outer if is false

  • File: routes/routes.js:971-1019
  • What: The outer if (result && Array.isArray(result) && result.length) at line 977 — if it evaluates false (no rows found), neither the if branch nor the else branch sends a response. The request hangs.
  • Why it matters: Any request for a non-existent or deleted user ID will hang indefinitely, leaking a connection/socket until the client times out.
  • Fix: Add a final else { res.send(responseObj); } after line 1018.

M6. decryptData in tokenGenerator.js has no try/catch — any malformed token throws an unhandled exception that leaks JWT internals

  • File: helper/tokenGenerator.js:12-17
  • What: jwt.verify throws JsonWebTokenError or TokenExpiredError on bad input. The function has no try/catch. In middlewares/auth.js, this is wrapped in a try/catch (line 29), so it is handled there. However, the webauthenticate handler at routes.js:308 calls decryptData inside a try/catch but only when reqObj.tempLogin && reqObj.token. If decryptData throws (e.g., token is well-formed but expired), the catch at line 327 re-attaches the Error object to responseObj.error and sends it to the client, leaking the full JWT error message and stack trace.
  • Fix: Wrap decryptData internals in a try/catch returning null on failure, rather than propagating the exception.

M7. updateUser interpolates five req.body fields directly into an UPDATE SQL statement

  • File: routes/auth.js:410
  • What: `update tMember SET sid='${reqObj.email}',firstName='${reqObj.fName}', lastName='${reqObj.lName}',role_type=${reqObj.role_type},role='${reqObj.role}' WHERE id=${reqObj.id}` — all six values are interpolated without escaping.
  • Why it matters: An admin-level caller can inject arbitrary SQL into the SET clause or WHERE id= to affect other rows. For example, reqObj.id = "1 OR 1=1" would update every row in tMember.
  • Fix: Parameterise: `UPDATE tMember SET sid=?, firstName=?, lastName=?, role_type=?, role=? WHERE id=?` with values array.

M8. getAccountNameList endpoint never sends a response if the query returns rows

  • File: routes/routes.js:677-695
  • What: The inner res.send(responseObj) call on line 689 is inside the if (getInitQuery && ...) block, but if getInitQuery is falsy (empty result set), res.send is never called. Control falls out of the inner if with no response. Additionally the outer if does not have an else branch for searchUser being null/empty.
  • Why it matters: Any search that returns no results hangs the connection.
  • Fix: Move res.send(responseObj) outside the inner if, or add an explicit else.

Low / nits

L1. new Buffer(data, 'base64') uses the deprecated Buffer constructor

  • File: routes/routes.js:116
  • What: new Buffer(...) was deprecated in Node.js 6 and removed in Node.js 18. Use Buffer.from(data, 'base64').
  • Fix: Replace with Buffer.from(data, 'base64').

L2. Multiple sync-mysql connections opened at module load time with no error handling

  • File: routes/routes.js:72-85, helper/helper.js:8-13, helper/aiLogics.js:6-12
  • What: Five separate sync-mysql connection objects (con, con1, and three in helpers) are created at module require time. None have connection-error callbacks. If the DB is unreachable at startup, the process loads silently and all con.query(...) calls subsequently throw synchronous exceptions that are either swallowed or unhandled.
  • Fix: At minimum, add a startup verification that these connections are reachable. Prefer the existing connectionPromise pool for new code.

L3. getAlltAccountMembers URL path parameter searchQuery allows SQL injection via LIKE pattern

  • File: routes/routes.js:1322
  • What: req.params.searchQuery is a URL path segment that can contain % and _ wildcards, and more critically can include ' to break out of the LIKE string. This is covered under C4 but is worth calling out separately because it comes from a URL path parameter (/:mId/:pId/:searchQuery), which many sanitisation patterns miss.

L4. searchAccountManagers query references Ac from inner subquery in outer WHERE clause

  • File: routes/routes.js:1377
  • What: `WHERE M.isDeleted = 0 AND M.role_type IN (6, 4) and Ac.accountName like '%${reqObj.search}%'`Ac is defined only in the inner LEFT JOIN subquery and is not in scope in the outer WHERE. MySQL may silently treat this as always-false or raise an error depending on the SQL mode. The query was probably intended to use A2.Accounts LIKE ....
  • Fix: Replace Ac.accountName with the appropriate alias from the outer scope, or restructure the join.

L5. analyzeKnowledge does not handle pyProg.on('error', ...) — unhandled spawn errors crash the process

  • File: routes/routes.js:640-674
  • What: If python is not found on PATH, spawn emits an error event. There is no pyProg.on('error', ...) handler, so this becomes an uncaught event emitter error, potentially crashing the server.
  • Fix: Add pyProg.on('error', (err) => { if (!responded) { responded = true; res.status(500).json({ status: false, message: err.message }); } });.

L6. getAllOwnerList in routes.js (the unauthenticated copy) will crash if req.body is absent

  • File: routes/routes.js:516
  • What: The GET /getAllOwnerList handler in routes.js (as opposed to the one in auth.js) accesses req.body.id at line 516, but GET requests typically have no body. If the Content-Type is not application/json or no body is sent, req.body is {} and req.body.id is undefined. The condition undefined > 0 is false, so the else branch runs, calling actions.insertData. This silently inserts a row in tPersonnel with no meaningful data on every GET /getAllOwnerList request — including from the frontend's normal pagination fetch.
  • Fix: This is the same root cause as M4. The insert/update logic does not belong in the GET handler.

Cross-cutting observations

  1. Auth middleware boundary: routes/auth.js correctly uses an Express Router with router.use(auth). routes/routes.js does the opposite — it binds routes directly to App.server (the raw Express app). This architectural split means every new endpoint added to routes.js is unauthenticated by default. The fix is to have routes.js use a sub-router with auth applied globally.

  2. SQL interpolation is the norm, not the exception: The codebase uses actions.queryAll({q:...${var}...}) for the vast majority of queries, and the queryAll wrapper passes the raw string to db.query(string, cb) with no parameterisation path. Switching to parameterised queries requires adding a params field to the queryAll call signature and threading it through actions.jsmodules/dbDriver/lib/mysql.js. This is a cross-cutting refactor.

  3. Passwords and hashes are selected and returned in API responses routinely: m.password appears in SELECT lists for user-listing, user-detail, and search endpoints. Hashed passwords are included in the JSON payload sent to the frontend. This is a systematic issue that needs a query-level audit across all SELECT statements.

  4. console.log of credentials in production paths: Lines 218, 221, and 254 in routes/routes.js are inside POST /webauthenticate and log the full request body (which contains passwords) and full DB result rows (which contain hashed passwords) on every login. These are not guarded by any NODE_ENV check and will ship to production log aggregators.

  5. Responses hang on no-data paths: Multiple handlers (getUserdetail, getAccountNameList, getAccountsList) do not call res.send when query results are empty or conditions fail. Hanging requests tie up sockets and can be weaponised for resource exhaustion.