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.comparefails (checkUser !== true), the code falls through toelse 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.passwordcan be logged in without knowing the bcrypt hash. Additionally, because the bcrypt callback callsres.sendfor both success and failure but does notreturnafter the role-check guard at line 255 (it sends a 403 and falls through), there are race-condition-style paths where twores.sendcalls can fire. The plaintext comparison branch is the most direct exploit: sendpassword: <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 (verifiedis null/undefined), the code sends a 403 response but does notreturn. Execution continues to line 314, whereverified.userIdis accessed (throwing aTypeError: Cannot read properties of null), which is caught by the outertry/catchat line 327, which then sends a 500 response body. The real problem is the missingreturn, which means ifdecryptDatasomehow succeeds but returns an unexpected truthy value, the code blindly proceeds to populateresponseObj.status = trueand return a valid session. - Why it matters: The correct guard is never enforced. Any token-decode error should stop execution.
- Fix: Add
returnbeforeres.send(responseObj)on line 312, so the branch exits after sending 403:
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 projectsomeli-web(client ID305153224854-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.jsonto.gitignore. Remove the file from git history (git filter-repoor BFG). Reference the secret only viaprocess.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, andreq.body.searchUserare interpolated directly into SQLLIKEclauses andWHEREconditions with no escaping or parameterisation. routes/routes.js:1278:`...name like '${reqObj.search}%'...`—reqObj.searchfromreq.bodyroutes/routes.js:1322:`...m.firstName like '%${req.params.searchQuery}%'...`— URL path paramroutes/auth.js:120-121:statusandplanfromreq.queryinserted intoWHEREclausesroutes/auth.js:595:searchfromreq.bodyinto multipleLIKEpredicatesroutes/routes.js:681:req.body.searchUserinto aLIKEclause viasync-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-mysqldriver does not automatically escape values; neither does thequeryAllwrapper used in most paths. A payload like'; DROP TABLE tMember; --would execute. - Fix: Replace all string interpolations with parameterised queries. For
sync-mysqluse the?placeholder syntax it supports. Foractions.queryAll, pass aparamsarray alongsideqand calldb.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.jscorrectly appliesrouter.use(auth)at line 72, gating every route in that file behind themiddlewares/auth.jstoken check. Butroutes/routes.jsregisters all its routes (/authenticate,/webauthenticate,/me,/addOrUpdatePersonnel,/getAllOwnerList,/getPersonnel/:pId,/updateProfile,/NewAccManager,/deleteAccount,/deleteUser,/addOrUpdateAffiliateDetails,/getAffiliateMarketingURL/:pId,/searchAffiliatePeople, etc.) directly onApp.server(the raw Express app) viaapiRoutes.prototype.init. There is norouter.use(auth)call anywhere inroutes.js. The single-route guardmethods.ensureTokenonly coversGET /(line 127). Every other route inroutes.jsis unauthenticated. - Why it matters:
POST /deleteUser,POST /deleteAccount,POST /updateProfile,POST /NewAccManager(elevates any user to account-manager role), andPOST /addOrUpdateAffiliateDetailsare 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 ofroutes.jsand apply it to every route that is not the login endpoints. The simplest approach is to use a sub-router for protected routes withrouter.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 ifbranch in the bcrypt callback grants a session token to any caller who setsreqObj.social = truein 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 validsid(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.
H4. GET /getAllOwnerList and related list queries return m.password to callers¶
- File:
routes/auth.js:256,routes/auth.js:753,routes/auth.js:817,routes/auth.js:897 - What: The
downlineQueryingetAllOwnerList(line 256) andsearchAccountUser(line 753) selectsm.passwordand includes it in the response payload. TheAccountManagersuserlist(line 817) andsearchUser(line 897) queries alsoSELECT m.passwordplus 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.passwordfrom allSELECTlists in these queries. Remove the'12345' AS auth_passliteral.
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, andtopiccome directly fromreq.bodywith no validation beyond nullity. While Node'sspawnwith an array does not invoke a shell (so classic; rm -rfshell injection does not apply),topicis an unbounded string that becomessys.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 ontopic. - Why it matters: The endpoint is unauthenticated (per H1). An unbounded string in
topicsent to a Python subprocess that readssys.argv[3]can cause OOM or unexpected behaviour depending on how the Python script uses it. - Fix: Add auth middleware. Validate
AccountIdandlanguageModelIdare integers; validatetopicis 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
closehandler at line 649 callsres.status(200).json(...)orres.status(500).json(...). Thestderr.on('data')handler at line 671 also callsres.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 secondres.jsoncall 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
pythonwarning output. - Fix: Use a flag
let responded = falseand check/set it before eachrescall. Alternatively, collect stderr in a buffer and only evaluate it inclose.
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
.gitignoremust be updated to prevent re-committing. - Fix: Add
conf/credentials.json(orconf/*.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}')`— bothreqObj.authorizationandsocialAuthcome fromreq.headers.authorization, a caller-controlled string, interpolated directly into the SQL query. - Why it matters: An attacker who can send arbitrary
Authorizationheader values (trivial) can inject SQL into thisWHEREclause. This endpoint is unauthenticated (see H1 — no auth middleware onroutes.js). SendingAuthorization: ' 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.emailis not validated or escaped. The same pattern appears at line 231 for thewebauthenticateendpoint withreqObj.emailandreqObj.sid. - Why it matters: The login endpoint is unauthenticated by design. A specially crafted email like
' OR '1'='1would return all rows, and the code then selectsresult[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 /addOrUpdatePersonnelhandler checks!req.bodyand returns 400, but otherwise falls off the end of the function without callingres.send. Ifreq.bodyis 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_Ralmost certainly calls this endpoint. Every call with a valid body hangs. There is also a separateGET /getAllOwnerListhandler inroutes.js(lines 445-548) that contains the realaddOrUpdatelogic body, meaning theGET getAllOwnerListhandler silently runs upsert logic ontPersonnelon what looks like a read request. - Fix: Move the insert/update logic from lines 515-547 into the
POST /addOrUpdatePersonnelhandler. TheGET /getAllOwnerListhandler 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 theifbranch nor theelsebranch 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.verifythrowsJsonWebTokenErrororTokenExpiredErroron bad input. The function has no try/catch. Inmiddlewares/auth.js, this is wrapped in atry/catch(line 29), so it is handled there. However, thewebauthenticatehandler atroutes.js:308callsdecryptDatainside atry/catchbut only whenreqObj.tempLogin && reqObj.token. IfdecryptDatathrows (e.g., token is well-formed but expired), the catch at line 327 re-attaches theErrorobject toresponseObj.errorand sends it to the client, leaking the full JWT error message and stack trace. - Fix: Wrap
decryptDatainternals in a try/catch returningnullon 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
SETclause orWHERE id=to affect other rows. For example,reqObj.id = "1 OR 1=1"would update every row intMember. - 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 theif (getInitQuery && ...)block, but ifgetInitQueryis falsy (empty result set),res.sendis never called. Control falls out of the innerifwith no response. Additionally the outerifdoes not have anelsebranch forsearchUserbeing null/empty. - Why it matters: Any search that returns no results hangs the connection.
- Fix: Move
res.send(responseObj)outside the innerif, 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. UseBuffer.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-mysqlconnection 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 allcon.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
connectionPromisepool for new code.
L3. getAlltAccountMembers URL path parameter searchQuery allows SQL injection via LIKE pattern¶
- File:
routes/routes.js:1322 - What:
req.params.searchQueryis 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}%'`—Acis defined only in the innerLEFT JOINsubquery and is not in scope in the outerWHERE. MySQL may silently treat this as always-false or raise an error depending on the SQL mode. The query was probably intended to useA2.Accounts LIKE .... - Fix: Replace
Ac.accountNamewith 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
pythonis not found onPATH,spawnemits anerrorevent. There is nopyProg.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 /getAllOwnerListhandler inroutes.js(as opposed to the one inauth.js) accessesreq.body.idat line 516, butGETrequests typically have no body. If the Content-Type is notapplication/jsonor no body is sent,req.bodyis{}andreq.body.idisundefined. The conditionundefined > 0is false, so theelsebranch runs, callingactions.insertData. This silently inserts a row intPersonnelwith no meaningful data on everyGET /getAllOwnerListrequest — 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
GEThandler.
Cross-cutting observations¶
-
Auth middleware boundary:
routes/auth.jscorrectly uses an ExpressRouterwithrouter.use(auth).routes/routes.jsdoes the opposite — it binds routes directly toApp.server(the raw Express app). This architectural split means every new endpoint added toroutes.jsis unauthenticated by default. The fix is to haveroutes.jsuse a sub-router with auth applied globally. -
SQL interpolation is the norm, not the exception: The codebase uses
actions.queryAll({q:...${var}...})for the vast majority of queries, and thequeryAllwrapper passes the raw string todb.query(string, cb)with no parameterisation path. Switching to parameterised queries requires adding aparamsfield to thequeryAllcall signature and threading it throughactions.js→modules/dbDriver/lib/mysql.js. This is a cross-cutting refactor. -
Passwords and hashes are selected and returned in API responses routinely:
m.passwordappears inSELECTlists 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 allSELECTstatements. -
console.logof credentials in production paths: Lines 218, 221, and 254 inroutes/routes.jsare insidePOST /webauthenticateand 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 anyNODE_ENVcheck and will ship to production log aggregators. -
Responses hang on no-data paths: Multiple handlers (
getUserdetail,getAccountNameList,getAccountsList) do not callres.sendwhen query results are empty or conditions fail. Hanging requests tie up sockets and can be weaponised for resource exhaustion.