designer-api — code inspection¶
Audit date: 2026-05-17. Lens: concrete code-level issues not already covered by someli-doc/audit/designer-api/security.md and friends.
Summary¶
22 findings across all severity levels. The most serious are: a plaintext password fallback in the authentication handler that lets any user authenticate by sending their stored (unhashed) password; a hardcoded Polotno commercial license key present in 20+ files; an isOnProcess flag that is never reset on the async code path that does the actual image processing work in the scheduler jobs (meaning the lock sticks permanently after the first real run); a download() error handler that silently swallows failures by returning an error object instead of throw-ing, causing callers that assume the return value is a Polotno JSON to crash in unpredictable ways; unguarded variantArray[0].rgbCode accesses that throw TypeError and freeze the isOnProcess lock across all 30+ industry-clone jobs; and a server.js DB connection guard that is logically inverted, so it never logs the actual error. Recurring themes: async work inside forEach losing the isOnProcess guard, hardcoded third-party keys in many files, SQL built from req.body string fields with no sanitisation in several endpoints, and AI prompt content flowing unchecked from req.body to OpenAI calls.
Critical¶
C1. Hardcoded Polotno commercial license key in 20+ source files¶
- File:
job_post_approved.js:68,job_preprodpost_approved.js:151,240,job_schedule_template.js:79,job_schedule_organization.js:79,job_swap.js:89,job_specific_approved_post.js:68,routes/routes.js:1261,2789,2869,3760,3840,3929,5891,8186,9179,13136, and ~5 more scripts (job_content_swap_entry.js:84,job_16_30_schedule_template.js:78,job_schedule_variant.js:55,polotno_image_uploader.js:31,s3_Image_upload.js:31) - What: The Polotno license key
FXZvloSJvAe09-bdR9iCis hardcoded as a string literal in every call tocreateInstance({ key: ... })throughout the codebase — at least 20 distinct occurrences. - Why it matters: The key controls billing and access to the Polotno rendering service. It is committed to version control and baked into the Docker image. Anyone with image or repo read access obtains a production billing credential.
security.mdF-9 noted it might be hardcoded but did not confirm; the key is confirmed present. - Fix: Move to
conf.js/process.env.POLOTNO_KEYand replace all literal occurrences withconf.POLOTNO_KEY.
C2. Plaintext password fallback in /webauthenticate bypasses bcrypt¶
- File:
routes/routes.js:212-219 - What: After
bcrypt.comparereturnsfalse, the handler falls through toelse if (reqObj.password == result[0].password)— a strict equality comparison of the plaintext input against the stored DB value. If any account has a plaintext password in the database (legacy migration, manually created row, or a reset operation that stored plaintext), this branch authenticates the user without any hash check. - Why it matters: The fallback entirely undermines bcrypt. A database compromise would immediately expose every account whose password is stored in plaintext. The branch also fires when an admin sets a password directly in SQL without hashing it.
- Fix: Remove lines 212-219 entirely. The only valid success branches are
checkUser === true(bcrypt match) and the social-auth branch (line 220). If plaintext accounts exist, migrate them.
C3. isOnProcess lock permanently stuck after first real run in job_post_approved.js and job_specific_approved_post.js¶
- File:
job_post_approved.js:20-112,job_specific_approved_post.js:20-112 - What: Both jobs have no
isOnProcessguard at all —getTempalteStatusList()fires on every cron tick with no lock. However, within each run thetAutoDesignPoststatus is set to'Added to Library'at line 111 unconditionally, outside the S3 upload callback. This means the record is marked done before the image is generated or uploaded. If Polotno or S3 fails, the record is permanently marked done with no image. More critically,instance.close()is called inside the S3 upload callback at line 94; if the upload callback errors, the instance leaks open. - Why it matters: Every approved post that hits an S3 or Polotno error is silently skipped with status
'Added to Library'— corrupting the pipeline permanently for that post. Polotno instances leak on S3 upload failure, consuming memory until the process is killed. - Fix: Move
con.query(UPDATE ... attachedLibrary = 1 ...)inside the success branch of the S3 upload callback. Wrap instance creation in try/finally to guaranteeinstance.close().
C4. download() returns an error object on failure instead of throwing — silent data corruption¶
- File:
job_post_approved.js:115-131, same pattern injob_preprodpost_approved.js,job_schedule_template.js,job_specific_approved_post.js,job_schedule_organization.js,routes/routes.js(thedownload1function at line ~99) - What: The
download()anddownload1()functions catch errors andreturn { statusCode: ..., body: ... }instead of re-throwing. Callers then pass this error object toJSON.stringify(),Buffer.from(), andinstance.jsonToImageBase64()as if it were a valid Polotno JSON document. - Why it matters: When S3 fetch fails (e.g., key not found, network error), the error object
{ statusCode: 400, body: "..." }is silently serialised as JSON and uploaded to S3 as a template, and then fed toinstance.jsonToImageBase64()which throws because it is not a Polotno document. The thrown error propagates out of the async callback and becomes an unhandled promise rejection. The record is still marked done. - Fix: Replace
return { statusCode: ... }withthrow errin alldownload()catch blocks. Each call site already has its own try/catch.
High¶
H1. Google OAuth client secret committed in conf/credentials.json¶
- File:
conf/credentials.json:8 - What: The file contains a live Google OAuth 2.0 client secret (
drGPpS_h7PCgekNpN5P3Fr5c) for projectsomeli-web. The file is tracked by git (it appears in the file listing from the repo, not in.gitignore). - Why it matters: Anyone with repo read access can impersonate the OAuth application, issue forged authorization codes, and redirect Google auth flows.
security.mdF-6 said "verify whether committed" — it is committed. - Fix: Rotate the client secret in Google Cloud Console immediately. Add
conf/credentials.jsonto.gitignore. Load client credentials from environment variables or Secret Manager.
H2. commonDelete and commonIsDeleted endpoints perform arbitrary table mutations with no auth check¶
- File:
routes/routes.js:303-320(/commonDelete),368-386(/commonIsDeleted) - What: Both endpoints take
req.body.tableandreq.body.idand executeDELETE FROM <table> WHERE id=<id>/UPDATE <table> SET isdeleted=1 WHERE id=<id>with no authentication check whatsoever. Any unauthenticated caller can delete or soft-delete any row in any table by specifying the table name and an id. - Why it matters: Full unauthenticated delete access to the entire database schema. An attacker who discovers these endpoints can destroy all library content, templates, users, or jobs.
- Fix: Add an auth check (token validation against
tMember.auth_token) before the query executes. These endpoints should be removed and replaced with specific typed endpoints that operate only on allowed tables.
H3. commonPagecount accepts arbitrary table name and WHERE clause from req.body¶
- File:
routes/routes.js:323-340 - What:
SELECT COUNT(id) AS rowcount FROM ${reqObj.table} WHERE ${reqObj.condition}— both the table name and the entire WHERE condition are taken directly fromreq.bodywith no authentication check and no sanitisation. - Why it matters: This is a full unauthenticated arbitrary read from any table with any condition. It can be used to enumerate data across all tables (time-based or boolean-based extraction of any column by constructing conditions like
id=1 AND SUBSTRING(password,1,1)='a'). - Fix: Auth check + restrict to an allowlist of table names and structured condition parameters.
H4. Unguarded variantArray[0] access causes TypeError in all industry-clone jobs¶
- File:
job_disability_insurance.js:47,job_motor_insurance.js:47,job_travel_insurance.js:47, and the same line in all ~30 industry-clone jobs - What:
con.query(... ORDER BY RAND() LIMIT 1)may return an empty array if all variants are already used for the day. The next line unconditionally accessesvariantArray[0].rgbCodewithout checking length. This throwsTypeError: Cannot read property 'rgbCode' of undefined. - Why it matters: In
job_disability_insurance.jsand most clones, theisOnProcessflag is set tofalseonly at the end of theindustryData.forEachloop — butforEachwith async callbacks does not await them. The thrownTypeErrorpropagates out of the synchronousforEachcallback; the outer function catches nothing. The real impact is a noisy unhandled rejection per callback invocation and the entire template insertion step being skipped silently for that category. - Why it's distinct from known F-8: F-8 documents the general async/forEach pattern; this is a specific, concrete
TypeErrorcaused by an unguarded array access that happens when the variant pool is exhausted — a routine operational condition. - Fix: Add
if (!variantArray || variantArray.length === 0) continue;(orreturninside the forEach callback) before accessingvariantArray[0].
H5. SQL injection via req.body.text, req.body.category, req.body.searchTerm in image search endpoints¶
- File:
routes/routes.js:1499,1580,1608,1620,1703,1705,1810,1812,1828,1845,1847 - What: Multiple endpoints (
/unsplashsearchimages,/pixabaysearchimages,/pexelssearchimages,/searchnewimages) interpolatereq.body.text,req.body.category,req.body.searchTerm,req.body.tableName, andreq.body.trowIddirectly into SQL strings sent to the synchronouscon.query()(which issync-mysql). For example:`SELECT * FROM tMediaLib WHERE searchTerm LIKE '${req.body.searchTerm}%' AND category ="${req.body.category}"` - Why it matters: A caller can inject arbitrary SQL. Because there is no auth middleware (F-3), these endpoints are reachable unauthenticated. The combination gives full unauthenticated SQL injection.
- Fix: Use parameterised queries via
mysql2(which is already imported) for all of these endpoints.
H6. checkUserPassword leaks the plaintext password in the query and the response¶
- File:
routes/routes.js:435,442 - What: The endpoint queries
WHERE tMemberAuth.sid='${email}' AND tMember.password='${password}'— comparing the supplied plaintext password against the stored hash as a string equality, which always fails for bcrypt hashes. Beyond being broken, it logs the password as part of the SQL string in any error path. The response at line 446 returnsresponseObj.data = responseDatawhereresponseDataisJSON.parse(JSON.stringify(result[0]))— the fulltMemberrow including thepasswordcolumn. - Why it matters: The hashed password of the user is returned to the caller in the response body. Any caller who can reach this endpoint receives the bcrypt hash, which is useful for offline cracking.
- Fix: Select only needed columns. Use bcrypt for comparison. Never return the
passwordfield.
H7. generateTextContent.js error handler crashes and masks the real error¶
- File:
generateTextContent.js:58 - What: The catch block does
console.log("ERROR : ", error.response.data.error). Iferror.responseis null (e.g., network timeout, DNS error), this line itself throwsTypeError: Cannot read properties of undefined, masking the original error.isOnProcessis then never reset because line 65 (isOnProcess = false) is in the normal flow and is skipped. - Why it matters: A network error to OpenAI permanently locks the job — no more content generation until the process restarts. The actual error is invisible.
- Fix: Use
error?.response?.data?.error ?? error.messageand always placeisOnProcess = falsein afinallyblock.
Medium¶
M1. job_schedule_template.js: status set to 'Completedd' (typo) and race with setTimeout¶
- File:
job_schedule_template.js:110-113 - What: Two
con.query(UPDATE tTempalte_Status)calls are made: one inside asetTimeout(..., 10000)setting status to'Completed', and one immediately after thesetTimeoutregistration (line 113) setting status to'Completedd'(double-d typo). BecausesetTimeoutis non-blocking, the immediate call at line 113 fires first — so the record is immediately set to'Completedd'before any processing occurs. The callback insidesetTimeoutthen overwrites it with'Completed'10 seconds later if successful, but thegetTempalteStatusListquery at line 37 filters fortStatus = 'Inserted'— so the'Completedd'status is never picked up by any consumer. - Why it matters: Every record processed by
job_schedule_template.jsends up with status'Completedd'for ~10 seconds, then'Completed'. If the process restarts in that window, records are permanently left in'Completedd'.job_schedule_organization.js:111has the same pattern. Affected records can never be reprocessed. - Fix: Remove the immediate
con.queryat line 113. Set status to'Completed'only inside the S3 upload success callback. Same fix applies tojob_schedule_organization.js:111.
M2. job_schedule_dynamic.js missing isOnProcess = false in the outer scope¶
- File:
job_schedule_dynamic.js:17-63 - What:
isOnProcessis set totrueat line 19, but there is noisOnProcess = falsein the outer async function after theindustryData.forEachloop completes (only a log message at line 62). TheisOnProcessflag is never reset tofalse, so the job runs exactly once and never again. - Why it matters: The dynamic post schedule runs once per process lifetime, not hourly as intended.
- Fix: Add
isOnProcess = falseafter the closing brace of the outerif(industryData ...)block and in a catch handler.
M3. job_gemini_Images.js: isOnProcess set after forEach, not awaited — job never actually locks¶
- File:
job_gemini_Images.js:36-46 - What: The
isOnProcess = trueassignment at line 39 is inside theif(getQuery...)block but after firinggetQuery.forEach(async(q) => { await gemini(q) }). TheforEachreturns immediately (not awaited).isOnProcess = falseis then set at line 45 before anygemini()call completes. So on the next tick,isOnProcessis alreadyfalseand the next cron tick starts another batch of up to 5 parallel Gemini calls, creating unbounded concurrency against the Gemini API. - Why it matters: Every cron tick (every minute) launches up to 5 more Gemini API calls even if the previous 5 haven't finished. Under load this fans out into dozens of concurrent AI API calls, causing runaway billing.
- Fix: Use
for (const q of getQuery) { await gemini(q); }and moveisOnProcess = falseto afinallyblock.
M4. server.js DB connection error handler is logically inverted — DB errors are silently ignored¶
- File:
server.js:26 - What:
db.connect(conf, (res) => { if (!res && !res.status) { console.log("Something Went Wrong!") } }). The condition is!res && !res.status— this requiresresto be falsy ANDres.statusto be falsy simultaneously. Ifresis falsy (null/undefined), the second operand!res.statuscauses aTypeError: Cannot read property 'status' of null/undefined. The condition is never actually true for a failed connection: the callback receives{ status: false, data: "Not Connected!" }on failure, so!resisfalseand the branch is never entered. DB connection failures produce no log message. - Why it matters: If the DB is down on startup, the server starts anyway and all requests silently fail. No operator alert.
- Fix:
if (!res || !res.status) { console.error("DB connection failed:", res); process.exit(1); }.
M5. helper/index.js: cropUpload uploads the raw input buffer as the "full" image rather than a processed JPEG¶
- File:
helper/index.js:40-44 - What:
helper/index.jsimports{con, s3}from../routes/routes(line 3), whereconis async-mysqlinstance. Then at line 40:let getCodeQuery = con.query(...)— butsync-mysql's query is synchronous, so this does return the result. However,cropUploadis called withinputImagePathas the binary image buffer but then passes the same buffer asBodytos3.uploadfor the full image (line 59), meaning the same buffer is uploaded to boththumbs/andimages/paths. The thumbnail (line 46-53) is the resized JPEG, but the "full" image (line 56-68) is the original unresized buffer — this is likely intentional for the full image but the code comment on line 59 saysBody: inputImagePathwhich is the already-consumed buffer reference, not a copy. - Why it matters: The full-image S3 key receives the raw binary input (which may be a URL buffer, not a JPEG), not the processed full-resolution JPEG. The image at the
images/S3 key will be corrupted or raw binary. - Fix: Sharp
.toBuffer()should be used for both the thumbnail and the full image, with the full image using.jpeg({quality:100})without.resize().
M6. content_generation_bot.js: AI output inserted directly into SQL via string interpolation¶
- File:
content_generation_bot.js:221 - What:
INSERT INTO tCopies(topicId,categoryId, caption, header, title, body) VALUES (${topicid}, ${categoryId}, "${q.Caption}", "${q.Header}", "${q.Title}", "${q.Body}")— the fieldsq.Caption,q.Header,q.Title,q.Bodyare LLM output inserted with double-quote string interpolation. LLM output containing double quotes or SQL metacharacters breaks the query or enables SQL injection. - Why it matters: AI-generated content commonly contains apostrophes, double quotes, and special characters. A crafted prompt or a model output containing
"followed by SQL terminates the string and injects arbitrary SQL into thetCopiestable. - Fix: Use parameterised queries. The same function already shows parameterised usage in
FAQsbot.js:135-138— use that pattern consistently.
M7. FAQsbot.js: debug console.log reveals raw interpolated SQL¶
- File:
FAQsbot.js:134 - What:
console.log("SQL : ", \INSERT INTO tCopies SET topicId=1,caption="${q.Caption}",header="${q.Header}",title="${q.Title}",body="${q.Body}")— while the actual INSERT at lines 135-138 is parameterised, the debug log constructs the same string-interpolated SQL and prints it to stdout (which goes to PM2 logs and potentially log aggregators). If any of these fields contain PII or sensitive content, it is logged in cleartext. - Why it matters: AI-generated content is logged in structured SQL form. Log aggregation systems (Slack, Datadog) will receive this. Separately, if LLM output ever contains passwords or PII from scraped content, it is logged.
- Fix: Remove the debug
console.logat line 134.
M8. Hardcoded Pixabay API key in multiple files alongside the DB-stored key lookup¶
- File:
job_schedule_template.js:353,job_schedule_organization.js:302,routes/routes.js:1408,1457 - What: Despite a
getkey("pixabay")function that fetches the active key fromtApiKeys, several places hardcodekey=40661313-6e6f015308502e262c449b0ecdirectly in the Pixabay API URL. Thegetkey()result is fetched but then ignored because the URL string uses the hardcoded key. - Why it matters: The hardcoded key bypasses the key rotation/error-fallback mechanism. When this key hits rate limits or is revoked, the fallback to the DB-stored replacement key never happens for these calls.
- Fix: Use
${pixabay_key}(already obtained viagetkey()) in the URL instead of the hardcoded value.
Low / nits¶
L1. routes/routes.js: titleCase() function is broken — references undefined splitStr¶
- File:
routes/routes.js:76-78 - What:
function titleCase(str) { return splitStr.join(' '); }—splitStris never defined in this function. The function throws aReferenceErroron every call. - Fix: Either implement it properly (
const splitStr = str.split(' ').map(...)) or delete it — it does not appear to be called anywhere in the routes.
L2. server.js: App.socket is always null at route init time¶
- File:
server.js:57-60 - What:
var socketConnection = null; ... App.socket = socketConnection—socketConnectionis captured asnullbefore any socket has connected. Even after a client connects andsocketConnection = socketis assigned,App.socketstill holdsnullbecause it was assigned by value. - Why it matters: Any route handler that uses
this.socketto emit events getsnulland throws. - Fix: Either pass
ioitself asApp.socketand emit onio, or makeApp.socketa getter function.
L3. job_check_color_with_json.js: isOnProcess set to true after firing async forEach — same non-lock pattern¶
- File:
job_check_color_with_json.js:29-38 - What:
getQuery.forEach(async(q) => { ... }); isOnProcess = true;—isOnProcessis set after the (non-awaited) async callbacks are fired. By the time anydownload()call starts,isOnProcessisfalseagain (line 40). Identical pattern injob_check_media_json.jsandjob_check_mediaTemplates_json.js. - Fix: Set
isOnProcess = truebefore theforEach, and add afinallyreset.
L4. job_preprodpost_approved.js: mediaType == 2 (video) branch initialises variables but does nothing¶
- File:
job_preprodpost_approved.js:206-217 - What: When
mediaType == 2, the code setspjson,pthumb,pvideopath variables but performs no uploads, no DB inserts, and no status updates. The job then falls through toisOnProcess = falsewithout error. Video posts are silently skipped. - Fix: Either implement the video processing path or log a warning and set the job status to a "video not supported" state so the record is not left in limbo.
L5. job_swap.js: isOnProcess reset races with in-flight callbacks¶
- File:
job_swap.js:34-76 - What: Inside
check_json_media(),isOnProcess = falseat line 57 fires only ifgetQueryis non-empty and theforEachfires. But inside theforEachcallback at line 51, thecon2.querysuccess callback setsisOnProcess = falseat line 57 — however the outerisOnProcess = falseat line 66 fires immediately after theforEachreturns (before anycon2.querycallback fires). SoisOnProcessis reset tofalseimmediately, but the in-flightcon2.querycallbacks may race with the next cron tick. - Fix: Use
for...of awaitand afinallyblock.
L6. generateTextContent.js and FAQsbot.js call deprecated openai.createChatCompletion API¶
- File:
generateTextContent.js:39,FAQsbot.js:114,content_generation_bot.js:110,190,job_schedule_template.js:336 - What:
openai.createChatCompletion(...)was the v3 SDK API. The installedopenaipackage (v4+) usesopenai.chat.completions.create(...). The v3 method does not exist on the v4 client; every call throwsTypeError: openai.createChatCompletion is not a function. - Why it matters: All content generation bots are broken at runtime. No content is generated.
- Fix: Replace
openai.createChatCompletion({model, messages})withawait openai.chat.completions.create({model, messages})and update response access fromresponse.data.choices[0].message.contenttoresponse.choices[0].message.content.
Cross-cutting observations¶
isOnProcess as a lock is structurally unreliable across the codebase. The flag is a module-level variable. It is set to true at the start of an async function, then passed through a mixture of sync forEach loops (which return before callbacks complete), setTimeout calls, and nested S3 callbacks. In most files the false reset fires before the real async work completes. The correct pattern (used only in job_tAutoDesignPost_delete_cron.js) is synchronous body + try/finally. All other jobs need the same treatment.
The download() function and its variants are copy-pasted into at least 8 separate files (job_post_approved.js, job_preprodpost_approved.js, job_specific_approved_post.js, job_schedule_template.js, job_schedule_organization.js, job_swap.js, job_content_swap_entry.js, routes/routes.js as download1), each with slight variations and some with the error-as-return-value bug (C4). A shared module would allow a single fix.
Hardcoded API keys across the board. In addition to the already-documented Slack token and Unsplash key, this inspection confirmed: Polotno license key (C1), Google OAuth client secret (H1), Pixabay key duplicated and hardcoded in URL strings bypassing the rotation mechanism (M8). Each of these was added by copy-paste during rapid development.
OpenAI SDK v3 API calls are used throughout the codebase despite v4 being installed (L6). This means every bot and generation job that calls openai.createChatCompletion silently fails at runtime. The createCompletion calls (for text-davinci-003) in routes/routes.js are also defunct because that model was retired by OpenAI in January 2024.
AI prompts accept req.body.content without sanitisation. /AISA, /ai_image, /Ai_quotes, /Ai_2030Agenda, /Ai_historicalFacts, /AiTweets, /generateTweets, /generatepov all pass raw user input directly to OpenAI prompts. There is no length cap, no content filtering, and no output validation. A caller can send arbitrarily large inputs (up to the 50 MB body limit), triggering large token counts and unexpected API costs.