← All repos

alist-cms

Strapi v4 headless CMS that backs the alist-website Vue frontend. Single “Initial commit” from Oct 2023 — stale, unmaintained, internet-reachable.

Primary stackNode.js / Strapi 4.14.x Last commit2023-10-12 Repo size3.0 MB Files209 Branchmaster Overall risk High

Executive summary

Tech stack

Strapi 4.14.x Node 16–20 Koa MySQL (mysql 2.18.1) i18n plugin users-permissions local upload provider koa2-ratelimit
CategoryTechnologyVersionNotes
CMS framework@strapi/strapi^4.14.4 (lockfiles 4.14.3 / 4.14.4)Released Oct 2023, ~18 months behind current 4.x; lockfiles disagree
RuntimeNode.js>=16, <=20.xNode 16 is EOL (Sep 2023); Node 20 still supported
Database drivermysql2.18.1Legacy callback driver; mysql2 also pulled in indirectly
Auth plugin@strapi/plugin-users-permissions^4.14.4Standard Strapi auth, JWT-based (HS256, default 30-day expiry)
i18n plugin@strapi/plugin-i18n^4.14.4Locales feature enabled
Upload provider@strapi/provider-upload-local^4.14.4Files written to public/uploads/ — served same-origin, not cloud-backed
Rate limitingexpress-rate-limit, koa2-ratelimit, rate-limiter-flexible7.1.0 / 1.1.3 / 3.0.0Three libraries pulled in; only koa2-ratelimit is actually wired
Admin UI@strapi/design-system^1.11.0Default Strapi admin, no customisations

Architecture

Stock Strapi v4 layout. server.js just calls strapi().start(). All business logic is convention-based: each folder under src/api/<name>/ declares a content type plus default controllers/routes/services via createCoreController/createCoreRouter. Two endpoints (contact-us, responses-career) override the default router to whitelist only POST/PUT and attach per-route rate-limit middlewares. Reusable field groups live under src/components/. Admin UI is the default Strapi panel — no customisation. Critically, src/index.js register/bootstrap are empty, so all access control lives in the runtime DB.

alist-cms/ ├── server.js one-liner bootstrap ├── package.json strapi 4.14.4, mysql, rate-limit libs ├── config/ │ ├── admin.js JWT/API/transfer secrets from env (no fallback) │ ├── api.js REST defaultLimit 25, maxLimit 100 │ ├── database.js mysql/mysql2/postgres/sqlite; user/pass default 'strapi' │ ├── middlewares.js stock Strapi middleware chain (bare cors/security/poweredBy) │ ├── plugins.js users-permissions ratelimit = malformed ← bug │ └── server.js host/port/APP_KEYS from env ├── src/ │ ├── index.js empty register/bootstrap ← no permission seed │ ├── admin/ only *.example.js (no overrides) │ ├── api/ 30 content types │ │ ├── page-* (12) CMS singletons (home, brand, creator, blog, …) │ │ ├── blog, blog-topic, career, team, team-group, faq variants │ │ ├── contact-us user-submission collection + ratelimit mw │ │ ├── responses-career CV upload collection + ratelimit mw (POST + PUT) │ │ └── test leftover debug content type ← shadow endpoint │ ├── components/ 21 component groups for reusable fields │ ├── extensions/ EMPTY (.gitkeep only) │ └── policies/ │ └── ratelimit.js dead code (v3-style, never registered) ├── database/migrations/ EMPTY (Strapi auto-syncs from schema.json) ├── public/uploads/ EMPTY in repo (gitignored) └── types/generated/ Strapi TS contentTypes & components

Security assessment — methodology

White-box static review of the full repository, grounded in the anthropic-cybersecurity-skills playbook library. Each OWASP/CWE dimension was scanned independently with grep/ripgrep over sources, routes, schemas, config and lockfiles, then every candidate finding was adversarially verified against the code before inclusion. Findings are deduplicated, mapped to OWASP Top 10 2021 + OWASP API Top 10 2023 and CWE, and scored with CVSS 3.1 (EPSS noted where a CVE applies). Confidence is downgraded where exploitability depends on the runtime up_permissions grants that are not in the repo.

OWASP Top 10 2021 OWASP WSTG OWASP API Top 10 2023 OWASP MASVS (mobile only) CWE CVSS 3.1 NIST CSF 2.0
Playbooks applied to this repo (server-side dimensions):
Access control: testing-for-broken-access-control · testing-api-for-broken-object-level-authorization · exploiting-idor-vulnerabilities · exploiting-broken-function-level-authorization · bypassing-authentication-with-forced-browsing · detecting-broken-object-property-level-authorization
Auth / session / token: testing-jwt-token-security · testing-for-json-web-token-vulnerabilities · exploiting-jwt-algorithm-confusion-attack · testing-api-authentication-weaknesses
Secrets & crypto: implementing-secret-scanning-with-gitleaks · detecting-aws-credential-exposure-with-trufflehog · performing-cryptographic-audit-of-application · testing-for-sensitive-data-exposure
SSRF / CORS / misconfig: exploiting-server-side-request-forgery · testing-cors-misconfiguration · testing-for-host-header-injection · testing-for-open-redirect-vulnerabilities
Supply chain / SCA: performing-sca-dependency-scanning-with-snyk · analyzing-sbom-for-supply-chain-vulnerabilities · detecting-supply-chain-attacks-in-ci-cd · prioritizing-vulnerabilities-with-cvss-scoring · implementing-epss-score-for-vulnerability-prioritization
Injection (scanned, clean): exploiting-sql-injection-vulnerabilities · exploiting-mass-assignment-in-rest-apis · exploiting-template-injection-vulnerabilities
Business logic: testing-for-business-logic-vulnerabilities · exploiting-race-condition-vulnerabilities

Risk narrative

alist-cms is a stock, unmaintained Strapi v4.14.x CMS (single Oct-2023 commit, no CI, no deploy automation) that backs the public alist.ae site and exposes anonymous job-application and contact forms handling PII. The most acute risk is a textbook unauthenticated write-IDOR/BOLA on PUT /responses-careers/:id combined with the fact that all authorization lives only in the runtime DB with no code-level policies, no bootstrap seed, and no CI gate — so a single admin-UI misconfiguration silently becomes a production exposure that this review cannot disprove. Brute-force protection on the credential endpoints is effectively absent: the users-permissions rate-limit config is malformed, the only correctly-built limiters are scoped to two non-auth routes, and the one global limiter is dead Strapi-v3 code. The framework and its transitive tree are ~18 months stale with multiple network-reachable CVEs (SSRF, info disclosure, admin XSS, axios/sharp/formidable/ReDoS on the upload path), and an unrestricted, unsized, web-served CV upload adds stored-XSS and DoS surface. No live secrets are committed and the secret-loading design fails-closed for admin/JWT keys, which caps the cryptographic exposure, but placeholder secrets in .env.example plus a hardcoded strapi:strapi DB-credential fallback remain deployment-hygiene hazards. Overall risk is High, dominated by the unauthenticated PII-integrity BOLA, the broken auth throttling, and the unpatched framework on an internet-reachable host.

Overall risk High
0 P0 4 P1 6 P2 7 P3

OWASP coverage

CategoryStatusNotes
A01:2021 Broken Access ControlVulnUnauthenticated write-IDOR/BOLA (AC-01), zero code-level authz with empty bootstrap (AC-05), debug test CRUD endpoint (AC-12), draft preview disclosure (AC-17).
A02:2021 Cryptographic FailuresVulnPII (CVs, contact data) stored at rest in clear with no private flag/encryption (AC-13); placeholder secrets in .env.example, HS256 forgeable if deployed (AC-07).
A03:2021 InjectionCleanAll controllers are default Strapi factories over a parameterized ORM; no raw SQL, no shell, no template injection sinks found in src/.
A04:2021 Insecure DesignPartialAppend-only submission collections expose an updatable PUT and rely entirely on runtime DB permissions with no code-enforced design; mass-assignment with no allowlist (AC-11).
A05:2021 Security MisconfigurationVulnWildcard CORS (AC-09), unrestricted/unsized web-served upload (AC-08), broken rate-limit config (AC-02), dead policy (AC-14), X-Powered-By + no HSTS/CSP (AC-15).
A06:2021 Vulnerable & Outdated ComponentsVulnStrapi 4.14.x with SSRF/info-disclosure/XSS CVEs (AC-03); vulnerable axios/follow-redirects/formidable/sharp/micromatch/braces/path-to-regexp transitives (AC-04).
A07:2021 Identification & Authentication FailuresVulnNo effective brute-force throttle on /api/auth/* (AC-02); long-lived non-revocable 30-day JWTs (AC-06); hardcoded strapi:strapi DB-credential fallback (AC-16).
A08:2021 Software & Data Integrity FailuresVulnpackage.json/lockfile divergence + dual conflicting lockfiles → non-reproducible build pulling unpinned esbuild-loader 4.x through install scripts (AC-10); no CI/SCA.
A09:2021 Security Logging & Monitoring FailuresPartialOnly the default Strapi logger; no auth-failure alerting, no APM/log forwarding, no monitoring of the (broken) rate limits. No code-level logging added.
A10:2021 Server-Side Request ForgeryPartialNo app-level SSRF sink in custom code; SSRF risk is inherited from the unpatched framework (CVE-2024-28186) and the vulnerable axios/follow-redirects chain (AC-03/AC-04).
API Top 10 (API1 BOLA / API2 BrokenAuth / API3 BOPLA)VulnAPI1 write-BOLA (AC-01), API2 broken auth throttling + long JWTs (AC-02/AC-06), API3 mass-assignment + draft over-exposure (AC-11/AC-17).

Detailed findings

P0 critical · 0 P1 high · 4 P2 medium · 6 P3 low / info · 7
AC-01

Unauthenticated write-IDOR / BOLA: PUT /responses-careers/:id lets anyone overwrite any job-applicant PII record

P1 API1:2023 BOLA (A01:2021) CWE-639 CVSS 8.1 EPSS n/a likely
Description

The responses-career collection stores job-application PII. Its router deliberately replaces createCoreRouter to expose POST (create) and PUT /responses-careers/:id (update by primary key). The PUT route carries only a rate-limit middleware — no policy, no auth flag — and the controller is the unmodified core controller whose update() performs no per-object authorization. The records have no owner relation, so even a custom check would have nothing to bind to. Because the matching POST endpoint must be callable by the anonymous Vue frontend (which sends no auth header), the Public role almost certainly also carries the 'update' action. Strapi default primary keys are auto-increment integers, so :id is trivially enumerable. This is a textbook write-side BOLA/IDOR. Confidence is 'likely' rather than 'confirmed' only because the live up_permissions grant for the Public role lives in the runtime DB (not in this repo); the code path is fully confirmed.

Evidence
src/api/responses-career/routes/responses-career.js:21-28 — custom router exposes PUT /responses-careers/:id -> handler 'responses-career.update', config.middlewares=['api::responses-career.ratelimit-career'] ONLY; no config.policies, no config.auth src/api/responses-career/controllers/responses-career.js:9 — module.exports = createCoreController('api::responses-career.responses-career'); default update(), no ownership check src/api/responses-career/content-types/responses-career/schema.json:14-56 — applicant PII (firstName, lastName, email[required], phoneNumber[required], currentLocation, cv media[required], jobId, jobTitle); no owner relation Verified: grep across src/ for ctx.state.user / isOwner / config.policies returns ZERO hits — no object-level authz anywhere in the codebase
Attack scenario
Attacker loads the public careers form once to learn the POST endpoint, then runs: for id in 1..N: PUT /api/responses-careers/{id} with a JSON body replacing email/phoneNumber/cv. With no auth and no ownership check, every applicant record is overwritten, hijacking recruiter communications and corrupting the applicant database.
Impact

Unauthenticated attacker iterates sequential :id values and overwrites any applicant's record: redirect recruiter follow-up by rewriting email/phone, swap the cv media pointer, forge jobId/jobTitle associations, or (via draftAndPublish) flip publication state. Mass integrity loss / poisoning of the recruitment PII pipeline with no authentication.

Remediation
Remove the PUT /responses-careers/:id route entirely — anonymous form submissions should be append-only POST. If editing is genuinely required, restrict it to authenticated/admin roles and add a custom policy enforcing ownership. In the users-permissions matrix, grant Public ONLY create on responses-career; remove update/find/findOne/delete. Add a startup assertion or test that fails if Public has write/read beyond create on submission collections.
Verifier note

Confirmed the route, controller, schema, and total absence of any policy/ownership code. Gated on the runtime Public-role grant which is not in-repo; kept 'likely'. This is the single highest-impact issue in the repo.

AC-02

Auth brute-force protection broken: malformed users-permissions ratelimit config; the only correctly-built limiters never touch /api/auth/*

P1 API2:2023 Broken Auth (A07:2021) CWE-307 CVSS 7.3 EPSS n/a confirmed
Description

Strapi v4 users-permissions auth endpoints read their rate-limit config from plugin.users-permissions.ratelimit, which config/plugins.js sets to { interval: 60000, max: 1 }. koa2-ratelimit expects interval as a duration object ({ min: 5 }) — exactly what the two working per-route middlewares in this same repo use — so the bare number 60000 is malformed and the window is coerced unpredictably. Two harms: (1) if it engages, max:1 permits a single auth request per window and self-DoSes the admin/user login; (2) the only correctly-built limiters are scoped to contact-us/responses-career and never to the credential endpoints, so there is no reliable brute-force / credential-stuffing throttle on /api/auth/*. The dead express-rate-limit policy provides none either. CLAUDE.md itself flags this config as broken.

Evidence
config/plugins.js:12-18 — 'users-permissions': { config: { ratelimit: { interval: 60000, max: 1 } } } src/api/contact-us/middlewares/ratelimit.js:36 and src/api/responses-career/middlewares/ratelimit-career.js:36 both use the CORRECT koa2-ratelimit shape interval: { min: 5 } / { min: 1 }, proving 60000 (raw ms) is malformed Both custom middlewares spread the plugin config via strapi.config.get('plugin.users-permissions.ratelimit') with `...rateLimitConfig` AFTER their own defaults (lines 42-43), so the plugin's max:1 OVERRIDES the per-route intent Both ratelimit middlewares are wired ONLY to contact-us POST and responses-career POST/PUT (their route files) — never to /api/auth/local, /auth/local/register, /auth/forgot-password, /auth/reset-password src/policies/ratelimit.js is dead (v3 shape, never registered) so provides no auth throttle either
Attack scenario
Attacker runs credential-stuffing against POST /api/auth/local. Because no functioning rate limit covers that path (the broken plugin config and the route-scoped limiters do not), thousands of password guesses proceed unthrottled; reset/confirmation tokens can likewise be brute-forced.
Impact

Either denial of service of the login/admin auth path (max:1 per malformed window) or, more likely, absence of an effective brute-force/credential-stuffing control on /api/auth/local, /auth/forgot-password, /auth/reset-password and /auth/email-confirmation — letting an attacker iterate passwords and guess OTP/reset/confirmation tokens without lockout.

Remediation
Set a valid koa2-ratelimit interval object, e.g. ratelimit: { interval: { min: 5 }, max: 5 }, keyed per-IP (not on user-controlled email). Ensure it actually engages on all /api/auth/* routes. Do NOT spread the global plugin ratelimit object over per-route configs after per-route defaults (it clobbers them). Add account lockout/backoff and monitor auth-failure rates. Re-test after the Strapi upgrade.
Verifier note

Confirmed config shape, the two correct middleware shapes, the override-by-spread bug, and that neither middleware is bound to auth routes. Merged the auth_jwt P1 and the ssrf_misconfig P2 koa2-ratelimit findings into this one canonical entry. Severity P1 for the brute-force gap; the self-DoS angle is the alternate manifestation of the same misconfig.

AC-03

Entire Strapi stack pinned to stale 4.14.x (Oct 2023) with multiple known CVEs (SSRF, info disclosure, XSS, privilege/data exposure)

P1 A06:2021 Vulnerable & Outdated Components CWE-1395 CVSS 8.2 CVE-2024-28186 / 25608 / 34065 / 31218 EPSS low single-digit % confirmed
Description

The application is entirely default Strapi: every controller/service is a stock factory, so the security posture is the framework's. The pinned line (4.14.3 in yarn.lock, 4.14.4 in package-lock.json) predates many 4.x security fixes. Applicable advisories for <=4.14.x include CVE-2024-28186 (SSRF in document/webhook handling), CVE-2024-25608 (information disclosure / returns private fields), CVE-2024-34065 (reflected XSS / path issue in admin), and CVE-2024-31218 (data exposure via populate against components/relations). Several are reachable without authentication given the public contact-us/responses-career/upload surface. Companion frontend hard-codes dev-strapi.alist.ae, so the instance is/was internet-reachable.

Evidence
package.json:14-17 — @strapi/strapi, plugin-users-permissions, plugin-i18n, provider-upload-local all pinned ^4.14.4 package-lock.json:16 / 2500 resolves @strapi/strapi, @strapi/admin, @strapi/plugin-users-permissions, @strapi/utils, @strapi/database to 4.14.4 yarn.lock:1835 pins @strapi/strapi@4.14.3 — the two committed lockfiles DISAGREE on the framework version (4.14.3 vs 4.14.4), both stale git log: two commits both 'Initial commit', last 2023-10-12; no dependency updates ever All controllers/services are default factories (verified: no non-default controller in src/api), so SSRF/path/info-disclosure attack surface lives in the unpatched framework itself
Attack scenario
An attacker fingerprints the X-Powered-By: Strapi banner (see AC-15), maps the version to a published 4.14.x advisory, and exploits the unauthenticated info-disclosure (CVE-2024-25608) or admin XSS (CVE-2024-34065) against dev-strapi.alist.ae to read private fields or hijack an admin session.
Impact

Depending on which patched CVE is reachable: SSRF pivot to internal services / cloud metadata (169.254.169.254), unauthenticated disclosure of private content-type fields, reflected XSS leading to admin-session takeover, and data exposure via populate. Worst realistic is admin-panel compromise of the full CMS.

Remediation
Upgrade the entire @strapi/* set to the latest 4.x patch (>= 4.25.x) immediately, then plan migration to Strapi 5 (4.x is in maintenance). Pin exact versions, consolidate to ONE lockfile, and gate npm audit / Snyk (severity high) in CI. Until patched, front the admin and content API with a WAF blocking RFC1918/link-local destinations and _q payloads, and require IMDSv2 on the host.
Verifier note

Merged the two duplicate stale-Strapi findings (ssrf_misconfig 4.14.3 + supply_chain 4.14.4). Confirmed both lockfiles, the version disagreement, and that all code is default-factory. CVE-2024-25608/34065 are well-established; CVE-2024-28186 SSRF reachability is configuration-dependent so impact band kept realistic.

AC-04

Vulnerable transitive dependencies on the request path (axios SSRF/token-leak, follow-redirects, formidable, sharp, micromatch/braces/path-to-regexp ReDoS)

P1 A06:2021 Vulnerable & Outdated Components CWE-1395 CVSS 7.5 CVE-2023-45857 / 2024-28849 / 4067 / 4068 / 45296 EPSS ~0.2–0.3% confirmed
Description

The resolved tree contains multiple packages with public CVEs, several directly on the internet-facing request path. axios 1.5.0 (CVE-2023-45857) and follow-redirects 1.15.3 (CVE-2024-28849) sit in server-side fetch/redirect paths; formidable 1.2.6 parses the multipart body of the public CV-upload and feeds sharp 0.32.0 for image decode; braces/micromatch (old majors present) and path-to-regexp 0.1.7 (via express) carry ReDoS. Combined with the broken rate limiting (AC-02), ReDoS and upload-decode paths are reachable unauthenticated.

Evidence
package-lock.json:4074 axios 1.5.0 (CVE-2023-45857 SSRF / XSRF-token leak on cross-origin redirect) — used by @strapi/admin, @strapi/plugin-upload package-lock.json:6868 follow-redirects 1.15.3 (CVE-2024-28849 credential leak across redirects) package-lock.json:6946 formidable 1.2.6 — multipart parser behind the public CV-upload endpoint package-lock.json:12617 sharp 0.32.0 — decodes attacker-supplied uploaded images via plugin-upload package-lock.json:4351 braces 3.0.2 + :6708 braces 2.3.2; :9414 micromatch 4.0.5 + :6805 micromatch 3.1.10 (ReDoS) package-lock.json:6458 path-to-regexp 0.1.7 nested under express 4.18.2 (CVE-2024-45296 ReDoS) — CONFIRMED present despite the top-level path-to-regexp being 6.2.1
Attack scenario
Attacker POSTs crafted input to a public endpoint to trigger ReDoS in micromatch/braces/path-to-regexp, exhausting the single Node event loop and taking the CMS offline; separately, a malicious uploaded image stresses the old sharp/formidable decode path.
Impact

ReDoS / resource-exhaustion DoS against public endpoints (compounded by the broken rate limit), SSRF and XSRF-token/credential leakage via the axios+follow-redirects chain, and memory-safety risk in the formidable->sharp upload-processing path that handles attacker-supplied files.

Remediation
Run npm audit / Snyk against the lockfile and remediate. Upgrading @strapi/* to latest 4.x floats most transitives forward (axios >=1.7.x, follow-redirects >=1.15.6, sharp >=0.33.x, micromatch >=4.0.8, braces >=3.0.3); for any that do not float, add npm overrides / yarn resolutions pinning safe versions. Force express's path-to-regexp to a patched line if it remains 0.1.7. Add CV-upload MIME/size limits and a working rate limit.
Verifier note

Confirmed every cited version against package-lock.json. Corrected the original finding's path-to-regexp evidence: the top-level package is 6.2.1, but 0.1.7 IS present nested under express, so CVE-2024-45296 stands. Folded the supply-chain transitive-deps finding here.

AC-05

Authorization fully externalized to runtime DB: zero code-level access control, empty bootstrap, no permission seed

P2 A01:2021 Broken Access Control CWE-862 CVSS 6.5 EPSS n/a confirmed
Description

Every content-type route relies on Strapi defaults, and the entire access-control decision surface (which actions each of Public/Authenticated may perform on each of the 30 content types) lives exclusively in the runtime up_permissions DB table. The repo has no policies, no auth flags, no bootstrap seeding, and no committed export of the role matrix. Authorization is unversioned, unreviewable, and not reproducible across environments. A single mistake in the admin UI (enabling find/findOne for Public on a sensitive collection, or leaving a freshly-created type 'open') silently becomes a production access-control flaw with no code-review or CI gate. The single Oct-2023 commit and lack of deploy automation make drift between documented intent and live grants highly likely. This is the root cause that turns AC-01 and AC-12 from 'likely' into real exposure.

Evidence
src/index.js:10,19 — register() and bootstrap() are empty; no programmatic permission/role config Verified: grep across src/ for config.policies / ctx.state.user / role / isOwner / scope returns ZERO functional hits (only the comment in src/policies/ratelimit.js) All routers are bare createCoreRouter(...) with no config.auth / config.policies (e.g. src/api/blog, page-home, career, creator-signup all confirmed default) git ls-files shows no committed permissions/roles/seed/.sql; database/migrations is empty
Attack scenario
During development the admin enables broad Public grants on responses-career/contact-us/test for convenience and forgets to lock them down; because nothing in code or CI checks the grant set, the open permissions ship to dev-strapi.alist.ae and are discoverable by forced browsing.
Impact

Access-control posture cannot be audited from source or enforced by code. Any operator mistake in the permissions UI directly exposes data with no safety net; environments cannot be guaranteed consistent; this review cannot definitively confirm the live grant set.

Remediation
Codify the role/permission matrix: seed Public/Authenticated grants programmatically in src/index.js bootstrap() (or a config plugin) so intended grants are version-controlled and reproducible. Add custom route policies for anything beyond public-read. Export and review the current production up_permissions table against intent. Add a startup assertion / test that fails the build if Public has write/find on submission or test collections.
Verifier note

Confirmed empty bootstrap, no policies, all default routers. This is a governance/coverage finding (CWE-862) and the umbrella under which the IDOR/forced-browsing items become exploitable. Kept at P2 as the realized impact is mediated by the more specific AC-01/AC-12.

AC-06

Long-lived JWTs (default 30-day expiry) with no revocation/blocklist and no refresh-token rotation

P2 API2:2023 Broken Auth (A07:2021) CWE-613 CVSS 5.4 EPSS n/a confirmed
Description

users-permissions is left at defaults: HS256 JWTs signed with JWT_SECRET and, with no jwt.expiresIn override, the stock 30-day lifetime. There is no token denylist, no logout-side invalidation, and no refresh-token rotation (no extension overrides the auth controller). The payload is a bare {id} with no aud/iss binding. A leaked/stolen token (e.g. via XSS in the paired alist-website, log leakage, shared device) stays valid up to 30 days and cannot be revoked server-side short of rotating JWT_SECRET (which kills ALL tokens). Password change does not invalidate prior tokens. Same pattern for admin sessions.

Evidence
config/plugins.js (full file, 23 lines) — no `jwt`/`jwtManagement` block under 'users-permissions'.config; only `ratelimit` is set, so users-permissions falls back to default issuer settings (HS256, 30-day expiry) Verified: grep for expiresIn/jwtOptions/jwtSecret/algorithm across src/ and config/ — no token-lifetime override anywhere src/extensions contains only .gitkeep — the users-permissions auth controller/jwt service is NOT customized, so stock 4.14.x behaviour applies config/admin.js:1-13 — admin auth.secret + token salts come from env with no rotation/revocation logic
Attack scenario
An XSS in the Vue frontend exfiltrates a logged-in user's JWT; because there is no expiry override or revocation, the attacker replays the token for up to 30 days even after the victim logs out or changes their password.
Impact

A stolen content-editor or end-user token grants persistent access for ~30 days with no single-session revocation; logout and password reset do not contain a compromise, widening the blast radius of any token leak.

Remediation
Set a short access-token lifetime in config/plugins.js ('users-permissions': { config: { jwt: { expiresIn: '1h' } } }) and adopt refresh-token rotation, or implement a server-side denylist (e.g. Redis keyed on jti) so logout/password-change revoke. Add and validate issuer/audience claims. Ensure password reset invalidates outstanding tokens.
Verifier note

Confirmed plugins.js has no jwt block and extensions is empty; stock 30-day HS256 behaviour applies. Valid hardening finding.

AC-07

Weak/placeholder signing secrets in .env.example ('tobemodified') — full auth bypass if deployed verbatim

P2 A02:2021 Cryptographic Failures CWE-330 CVSS 6.5 EPSS n/a theoretical
Description

All crypto material (HS256 JWT_SECRET for user tokens, ADMIN_JWT_SECRET for the admin panel, API_TOKEN_SALT, TRANSFER_TOKEN_SALT, and APP_KEYS for the Koa session cookie) is sourced from env with no fallback and no entropy validation. The committed .env.example ships dictionary-word placeholders. Because the token algorithm is HS256, JWT_SECRET is also the verification key: if any placeholder reached production (a common mistake, called out in this repo's own CLAUDE.md), an attacker could brute-force/guess the HS256 secret (hashcat -m 16500 against one captured token) and forge tokens for any user, and forge admin-panel JWTs via ADMIN_JWT_SECRET for full CMS takeover. The production value is not in the repo, hence theoretical confidence.

Evidence
.env.example:3-7 — APP_KEYS="toBeModified1,toBeModified2" / API_TOKEN_SALT=tobemodified / ADMIN_JWT_SECRET=tobemodified / TRANSFER_TOKEN_SALT=tobemodified / JWT_SECRET=tobemodified config/server.js:5 — app.keys = env.array('APP_KEYS') with no strength check config/admin.js:3-11 — auth.secret / apiToken.salt / transfer.token.salt all from env with no validation Verified: .gitignore:108 lists .env and `ls .env` => not present, so real secrets are NOT committed — this is a deploy-hygiene risk, not an in-repo leak
Attack scenario
An operator copies .env.example to .env on the dev box without changing JWT_SECRET; an attacker who captures any user token offline-cracks the dictionary-word secret and forges an admin JWT, taking over the CMS.
Impact

If placeholder/weak secrets reached production: full authentication bypass via forged HS256 user tokens, admin-panel JWT forgery (complete CMS compromise), and session-cookie forgery via guessable APP_KEYS.

Remediation
Generate high-entropy random values (>=256-bit, openssl rand -base64 32) for JWT_SECRET, ADMIN_JWT_SECRET, API_TOKEN_SALT, TRANSFER_TOKEN_SALT and two distinct APP_KEYS per environment. Rotate immediately if any 'tobemodified'-style value was ever deployed. Add a boot-time assertion rejecting known placeholder strings.
Verifier note

Confirmed placeholders in .env.example and that no real .env is committed. Theoretical because production secret values are out of repo. The 'no fallback / fail-boot' design in admin.js/server.js is good and noted.

AC-08

Unrestricted CV file upload: any file type, no size limit, served from web-accessible public/uploads/ via local provider

P2 A05:2021 Security Misconfiguration CWE-434 CVSS 6.5 EPSS n/a confirmed
Description

The career endpoint accepts a required cv media with allowedTypes:['files'], which in Strapi means arbitrary file types. The upload config sets no sizeLimit and no MIME allowlist, so Strapi falls back to the ~200MB default body limit and stores files via the local provider under public/uploads/, served directly by strapi::public. The route is publicly reachable (POST, rate-limited only, and that limiter is degraded — see AC-02). An attacker can upload very large files (disk-exhaustion DoS) or dangerous-extension files (.html/.svg/.js) served same-origin, enabling stored-XSS / HTML-injection and content-type confusion. Non-image files are not renamed to opaque paths with restricted Content-Type by default.

Evidence
src/api/responses-career/content-types/responses-career/schema.json:39-47 — cv media, multiple:false, required:true, allowedTypes:['files'] (='any file type', not images-only) src/api/responses-career/routes/responses-career.js:13-20 — public POST /responses-careers, only rate-limit middleware, no auth config/plugins.js:2-10 — upload provider 'local', localServer.maxage only; NO sizeLimit, NO MIME allowlist config/middlewares.js:11 — 'strapi::public' serves public/ statically; @strapi/provider-upload-local writes to public/uploads/
Attack scenario
Attacker POSTs a job application with cv=evil.html containing <script>; Strapi stores it at public/uploads/evil.html and serves it as text/html same-origin; an admin or victim who opens the file URL executes attacker JS in the CMS origin. Alternatively the attacker uploads repeated 100MB files to exhaust disk.
Impact

Disk-exhaustion DoS via oversized uploads; stored XSS / HTML smuggling via .html/.svg served same-origin from public/uploads/<name>; storage abuse. Availability/integrity impact on host and visitors.

Remediation
Add upload.config.sizeLimit (e.g. 5MB) and a server-side MIME/extension allowlist; restrict schema allowedTypes to the formats actually needed (enforce pdf/doc via a beforeCreate hook). Serve uploads from a non-executable path or object storage (S3 with Content-Disposition: attachment on a separate domain), with random filenames and forced-download Content-Type.
Verifier note

Confirmed schema allowedTypes:['files'], no sizeLimit in plugins.js, local provider + strapi::public. Folded the existing-report 'CV uploads no virus scanning' item in here.

AC-09

Default wildcard CORS (bare strapi::cors) on the public content API

P2 A05:2021 Security Misconfiguration CWE-942 CVSS 5.3 EPSS n/a confirmed
Description

CORS is registered with the bare default strapi::cors and no origin allowlist. Strapi 4's built-in default is Access-Control-Allow-Origin: * (methods GET,POST,PUT,DELETE,PATCH,HEAD). Any web origin can script-read responses from every exposed /api/* endpoint. The content API is bearer/JWT-based rather than cookie-based, so credentialed-CORS session theft is not directly possible (which caps severity), but the wildcard still lets arbitrary origins read all publicly-permitted CMS content and any data exposed to a leaked/embedded API token, and removes a defense-in-depth boundary. The only legitimate consumer is the alist-website Vue frontend, so an explicit allowlist is feasible.

Evidence
config/middlewares.js:4 — 'strapi::cors' as a bare string, no { config: { origin: [...] } } find config -type d returns only 'config' — no config/env/production/middlewares.js override exists config/middlewares.js is the unmodified Strapi default stack (verified all 10 lines)
Attack scenario
A visitor logged into the CMS-backed app browses attacker.com, whose JS issues fetch('https://dev-strapi.alist.ae/api/...') and reads the response cross-origin because ACAO:* permits it.
Impact

Any third-party site can read all CORS-exposed CMS API data in a visitor's browser context; combined with a leaked API token an attacker page could read token-scoped data cross-origin. Weakens CSRF/clickjacking posture.

Remediation
Set an explicit origin allowlist: replace 'strapi::cors' with { name: 'strapi::cors', config: { origin: ['https://alist.ae','https://www.alist.ae'], methods: ['GET','POST','PUT'], headers: ['Content-Type','Authorization'] } }. Never reflect arbitrary Origin and never combine * with credentials.
Verifier note

Confirmed bare strapi::cors and no env override. Strapi 4 default ACAO is *. Valid; severity capped by JWT (non-cookie) auth model.

AC-10

package.json / lockfile divergence: esbuild-loader declared ^4.0.0 but locked to 2.21.0; two competing lockfiles committed

P2 A08:2021 Software & Data Integrity Failures CWE-1104 CVSS 6.5 EPSS n/a confirmed
Description

package.json declares esbuild-loader ^4.0.0, but both committed lockfiles resolve 2.21.0 — the manifest was edited after the lockfiles were generated. Consequences: (1) `npm ci` aborts with EUSAGE because lock and manifest disagree, breaking reproducible/CI builds; (2) a fresh `npm install`/`yarn install` silently re-resolves esbuild-loader to whatever 4.x is latest at build time — an unpinned, never-reviewed dependency that runs as part of the admin build (strapi build), widening the supply-chain trust surface; because several packages carry hasInstallScript:true, a hijacked release would execute arbitrary code at install time. Compounding this, the repo ships two lockfiles that also disagree on the Strapi version, so the actual production dependency tree is undefined.

Evidence
package.json:23 — "esbuild-loader": "^4.0.0" package-lock.json:6093 — esbuild-loader resolved 2.21.0 (and :2416 / :16892 pin ^2.21.0) yarn.lock:4363 — esbuild-loader@^2.21.0 (resolved 2.21.0) — present, contradicting manifest's ^4.0.0 Repo ships BOTH yarn.lock and package-lock.json, and they also disagree on @strapi/strapi (4.14.3 vs 4.14.4) package-lock.json hasInstallScript:true for sharp 0.32.0, esbuild, @strapi/strapi, fsevents, core-js-pure, vite/esbuild — install scripts run during build with no integrity gate
Attack scenario
A build server runs npm install (not npm ci), which honors package.json's ^4.0.0 and pulls a freshly-published, compromised esbuild-loader 4.x whose install/build hooks run arbitrary code during strapi build, backdooring the admin bundle.
Impact

Builds are non-reproducible and the production dependency tree is undefined. A future malicious esbuild-loader 4.x release (or one of its deps) would be pulled automatically with no lockfile guardrail, executing in the build toolchain via lifecycle scripts. CI/dev/prod artifacts can silently diverge.

Remediation
Pick ONE package manager and delete the other lockfile. Reconcile package.json with the chosen lockfile: either set esbuild-loader back to ^2.21.0 to match, or bump the lock to a vetted 4.x and re-commit. Pin build-critical toolchain packages to exact versions. Enforce npm ci / yarn install --frozen-lockfile in CI so divergence fails the build and lockfile integrity hashes are verified.
Verifier note

Confirmed the version mismatch in package.json vs both lockfiles. Corrected the original finding's claim that yarn.lock omits esbuild-loader — it is present as ^2.21.0. Folded the dual-lockfile/Strapi-version-disagreement and the postinstall-scripts findings here as the same software-integrity issue.

AC-11

Mass-assignment on anonymous submission collections: default core create/update binds all writable fields with no allowlist

P3 API3:2023 BOPLA / Mass Assignment CWE-915 CVSS 4.3 EPSS n/a likely
Description

The submission endpoints use the default core create/update, which bind every non-system attribute from the request body without a server-side writable-field allowlist. For these anonymous forms an attacker controls every field including ones the frontend never surfaces (jobId/jobTitle on responses-career; company on contact-us) and can supply arbitrary/oversized values (the richtext message accepts attacker markup, feeding stored-content risk in any admin viewer). The schema constrains assignment to declared attributes (so no privilege field like 'role' exists to escalate), but the lack of an allowlist plus the unauthenticated PUT (AC-01) lets an attacker set application-meaningful fields (forge a jobId/jobTitle association) that downstream business logic trusts.

Evidence
src/api/contact-us/routes/contact-us.js:11-18 — POST /contact-uses -> default contact-us.create, only rate-limit middleware src/api/responses-career/routes/responses-career.js:13-28 — POST/PUT -> default controller responses-career schema includes jobId (biginteger) / jobTitle (string) writable by client; contact-us schema includes message (richtext) and company Verified: grep for sanitize / sanitizeInput / field allowlist across src/api/*/controllers — no matches; all controllers are default factories
Attack scenario
Attacker POSTs a career response with a jobId/jobTitle of their choosing pointing at a different requisition, mislabeling applications; or posts richtext message markup that renders in the admin review UI.
Impact

Submission metadata integrity is attacker-controlled: forged job associations, spoofed company/message content, and data-quality/poisoning of the recruitment and contact pipelines. No direct privilege escalation (no sensitive property in these schemas).

Remediation
Override create/update for contact-us and responses-career with an explicit writable-field allowlist (ctx.request.body = pick(allowed, body)) and server-side validation (length, MIME for CV). Derive server-trusted fields (jobId/jobTitle) from a lookup rather than the client body, and reject unexpected keys.
Verifier note

Confirmed default controllers and no sanitize/allowlist anywhere. Impact is limited because schemas have no privilege fields; kept P3. Closely related to AC-01.

AC-12

Debug 'test' content type ships with default public CRUD router (forced-browsing / shadow endpoint)

P3 A01:2021 Broken Access Control CWE-489 CVSS 4.3 EPSS n/a likely
Description

A leftover debug content type `test` ships with the standard createCoreRouter, registering the full CRUD route set. It is not linked from the frontend and serves no product purpose, yet it is a live, enumerable endpoint (/api/tests, /api/tests/:id). If the Public or Authenticated role retains any grant (the default freshly-created state, or an accidental enable), it becomes an unauthenticated CRUD surface reachable by forced browsing. Read-only exposure leaks an attack surface; write exposure allows planting arbitrary records. Likely (not confirmed) because live reachability depends on the out-of-repo up_permissions grants.

Evidence
src/api/test/routes/test.js:9 — module.exports = createCoreRouter('api::test.test'); registers full GET/POST/PUT/DELETE /tests and /tests/:id src/api/test/controllers/test.js — default core controller src/api/test/content-types/test/schema.json — displayName 'test', draftAndPublish true, fields title + repeatable blog.information component CLAUDE.md explicitly flags src/api/test/ as a debug content type to verify is not exposed
Attack scenario
Attacker forced-browses /api/tests; if Public retains the default create grant from dev, POSTs arbitrary records to plant content or probe behaviour.
Impact

Unnecessary attack surface; potential unauthenticated enumeration or data injection depending on the live grant. Low data sensitivity (debug content) but indicative of poor endpoint hygiene.

Remediation
Delete src/api/test/ entirely before any redeploy. Confirm no Public/Authenticated grants exist on api::test.test, and add a CI check that rejects content types named test/debug/temp.
Verifier note

Merged the two duplicate 'test content type' findings (access_control + ssrf_misconfig). Confirmed default CRUD router and schema; reachability gated on DB grants -> likely. CWE-489 (active debug code) chosen over CWE-1059.

AC-13

Applicant/contact PII (CV uploads, name, email, phone, message) stored at rest with no field-level privacy or encryption

P3 A02:2021 Cryptographic Failures CWE-311 CVSS 3.1 EPSS n/a confirmed
Description

The two user-submission collections store personal data (job-applicant resumes plus identity/contact details, and contact-form messages) as ordinary Strapi attributes with no `private:true` flag and no application-level encryption. CV files are written in clear to public/uploads via the local provider. This is sensitive-data-at-rest with no cryptographic protection. It is standard out-of-the-box Strapi behaviour, hence hardening/informational, but it is the only PII-handling surface in the repo and is relevant under GDPR/data-protection expectations.

Evidence
src/api/responses-career/content-types/responses-career/schema.json:15-47 — firstName, lastName, email[required], phoneNumber[required], currentLocation, cv media; no `private:true`, no encryption src/api/contact-us/content-types/contact-us/schema.json:14-29 — firstName, lastName, email, company, message as plain attributes config/plugins.js:5 — upload provider 'local'; CV files land on the filesystem under public/uploads with no at-rest encryption All content controllers are default createCoreController — no transformResponse/private-field redaction
Attack scenario
An attacker who learns or enumerates an upload filename fetches https://dev-strapi.alist.ae/uploads/<cv>.pdf directly with no auth, reading an applicant's resume; a DB read via any other flaw exposes all contact/applicant PII in clear.
Impact

Any compromise yielding DB or filesystem read access (a Strapi CVE per AC-03, the upload path per AC-08, or infra exposure) discloses applicant resumes and contact PII in plaintext. Because the local provider stores uploads under the public web root, file URLs are unauthenticated once known.

Remediation
Mark non-display PII fields private:true to exclude them from public API serialization. Store uploads outside the public web root or behind authenticated access, preferring an object store with server-side encryption (or encrypt the volume). Apply a data-retention/deletion policy for CVs. At minimum ensure DB and uploads volume are encrypted at rest at the infrastructure layer.
Verifier note

Confirmed schemas have no private flag and the local provider serves from the public root. Genuine hardening/PII finding; kept P3.

AC-14

Dead Strapi-v3 express-rate-limit policy creates a false sense of brute-force protection

P3 A05:2021 Security Misconfiguration CWE-1164 CVSS 3.1 EPSS n/a confirmed
Description

src/policies/ratelimit.js implements an express-rate-limit limiter using the Strapi v3 initialize() contract and strapi.app.use(...). Strapi v4 does not load policies this way and the file is never registered against any route, so it never executes (and express middleware would not even work on Koa). Its presence — plus three competing rate-limit packages in package.json — is a maintenance hazard: a reviewer may believe a global 1-req/min limiter is enforced when it is not, masking the auth brute-force gap (AC-02). CLAUDE.md itself flags it as dead code.

Evidence
src/policies/ratelimit.js:4-23 — require('express-rate-limit'); module.exports = (strapi) => ({ async initialize(){ ... strapi.app.use(limiter) } }) — legacy Strapi v3 policy/initialize shape Strapi v4 does not load src/policies/*.js with an initialize() lifecycle; verified no route lists this policy in config.policies (grep returns only the file itself) package.json:18,22 declares express-rate-limit AND rate-limiter-flexible (and koa2-ratelimit) — three overlapping libs, only koa2-ratelimit is wired
Attack scenario
Not directly exploitable; the harm is that the dead file lulls maintainers into skipping a real auth rate limit, leaving AC-02 unaddressed.
Impact

Misleads operators into believing global rate limiting exists; contributes to the absence of effective brute-force protection on auth endpoints. No direct exploit.

Remediation
Delete src/policies/ratelimit.js and remove the unused express-rate-limit and rate-limiter-flexible packages from package.json, standardizing on the koa2-ratelimit middleware pattern and applying it to the auth routes. Document the single intended rate-limit mechanism.
Verifier note

Merged the two duplicate dead-policy findings (auth_jwt + ssrf_misconfig). Confirmed v3 shape and that nothing references it.

AC-15

Default Strapi security middleware: version disclosure (X-Powered-By) and no hardened CSP/HSTS

P3 A05:2021 Security Misconfiguration CWE-693 CVSS 3.7 EPSS n/a confirmed
Description

The middleware stack keeps default strapi::poweredBy (emits an X-Powered-By: Strapi banner that aids fingerprinting and compounds the stale-version finding AC-03) and bare strapi::security (helmet) with no explicit Strict-Transport-Security, no tightened Content-Security-Policy, and default frameguard. There is no production env override. For a public deployment this leaves header-level hardening at framework defaults and advertises the stack to attackers enumerating CVEs.

Evidence
config/middlewares.js:5 — 'strapi::poweredBy' emits X-Powered-By: Strapi config/middlewares.js:3 — 'strapi::security' bare default helmet config, no HSTS/CSP override find config -type d shows no config/env/production override of either middleware
Attack scenario
Attacker curls the site, reads X-Powered-By: Strapi, infers the framework, and pairs it with the version inferred from behaviour to select a matching 4.14.x exploit (AC-03).
Impact

Technology/version fingerprinting accelerates targeted exploitation of the unpatched Strapi version; absence of explicit HSTS/CSP reduces defense-in-depth against TLS-stripping and injection.

Remediation
Remove 'strapi::poweredBy' (or set poweredBy.enabled=false). Configure 'strapi::security' with explicit helmet: contentSecurityPolicy directives, hsts { maxAge: 31536000, includeSubDomains: true, preload: true }, frameguard. Also terminate TLS and add HSTS at the edge proxy.
Verifier note

Confirmed both middlewares are bare defaults with no override. Standard hardening finding; P3.

AC-16

Database credentials fall back to hardcoded default user/password 'strapi'/'strapi' when env vars are unset

P3 A07:2021 Identification & Authentication Failures CWE-1392 CVSS 3.7 EPSS n/a confirmed
Description

All three DB connection blocks (mysql, mysql2, postgres) default the database username AND password to the literal string 'strapi'. Unlike the JWT/admin secrets (which have no fallback and fail boot when unset), the DB password silently degrades to a publicly-known default. If an operator deploys without explicitly setting DATABASE_PASSWORD (easy, since the app still boots), the production account is protected by the well-known pair strapi:strapi. This is a hardcoded-default-credential weakness (CWE-1392), not a committed live secret.

Evidence
config/database.js:13-14 — user: env('DATABASE_USERNAME','strapi'), password: env('DATABASE_PASSWORD','strapi') config/database.js:34-35 (mysql2) and :56-57 (postgres) repeat the same 'strapi'/'strapi' defaults Contrast with config/admin.js / config/server.js which have NO fallback and fail boot when their secrets are unset
Attack scenario
An operator deploys to a box where the DB listens on a shared network and forgets to set DATABASE_PASSWORD; an attacker on that network connects as strapi:strapi and dumps all applicant/contact PII.
Impact

If the DB is reachable (same host/VPC, misconfigured bind, managed DB without network ACLs) and the operator relied on the default, an attacker guessing strapi:strapi gains full read/write to all CMS content including the contact-us and responses-career PII tables. Exploitability depends entirely on DB network exposure, hence low severity in a code review.

Remediation
Remove the fallbacks: password: env('DATABASE_PASSWORD') with no default, so Strapi fails fast when the secret is absent (mirroring ADMIN_JWT_SECRET/API_TOKEN_SALT). Enforce a strong random DB password from the deployment secret manager. Ensure the database is not reachable outside the application network.
Verifier note

Confirmed the literal 'strapi' fallbacks in all three blocks. Low severity, gated on DB network exposure.

AC-17

Draft content disclosure via publicationState=preview on default find/findOne

P3 API3:2023 BOPLA CWE-200 CVSS 4.3 EPSS n/a theoretical
Description

Almost every content type enables draftAndPublish and uses unmodified core controllers. Strapi's default find/findOne honor the client-supplied publicationState=preview parameter, returning unpublished (draft) entries alongside published ones. If the Public role has read on these types (normal for a public marketing site reading published pages/blogs), an anonymous client can append ?publicationState=preview to retrieve draft/unpublished content (unreleased blog posts, page revisions, embargoed careers). Theoretical because it depends on the live Public read grant and on whether drafts exist.

Evidence
Verified: 30 of 30 content-type schemas under src/api set options.draftAndPublish true All content controllers are default createCoreController (no transformResponse/sanitize override) config/api.js:5 — rest.maxLimit 100 permits large list pulls No custom logic restricts the publicationState query parameter
Attack scenario
Attacker requests GET /api/blogs?publicationState=preview and reads embargoed/draft posts before their intended release.
Impact

Disclosure of unpublished marketing content, draft blog posts, or not-yet-live page copy to anonymous users, undermining embargoes and editorial workflow confidentiality.

Remediation
Add a policy/middleware that strips or forbids publicationState=preview for non-editor requests, or override find/findOne to force publicationState=live for the Public role. Restrict large list pulls and confirm the Public read scope.
Verifier note

Confirmed 30/30 draftAndPublish and default controllers. Theoretical pending live Public read grant; kept as such.

Remediation roadmap

Immediate
Stop the unauthenticated PII-integrity exposure and lock the runtime grant set.
  • AC-01: Remove the PUT /responses-careers/:id route; grant Public ONLY create on submission collections; remove update/find/findOne/delete.
  • AC-05/AC-12: Export and review the production up_permissions table against intent; delete src/api/test/ and confirm no Public/Authenticated grant on api::test.test.
  • AC-07: Rotate JWT_SECRET / ADMIN_JWT_SECRET / API_TOKEN_SALT / TRANSFER_TOKEN_SALT / APP_KEYS immediately if any 'tobemodified' placeholder ever reached the deployed .env; regenerate with openssl rand -base64 32.
This week
Close the brute-force gap and patch the known-vulnerable framework.
  • AC-02: Fix the koa2-ratelimit config to interval: { min: 5 }, max: 5, bind it to all /api/auth/* routes keyed per-IP, and add account lockout/backoff.
  • AC-03/AC-04: Upgrade all @strapi/* to latest 4.x (>=4.25.x) to float axios/follow-redirects/sharp/micromatch/braces forward; add overrides/resolutions for any that do not float (force express's path-to-regexp off 0.1.7).
  • AC-08: Add upload sizeLimit and a server-side MIME/extension allowlist; restrict CV schema allowedTypes to pdf/doc.
This month
Establish reproducible builds, codified authz, and an explicit network boundary.
  • AC-05/AC-11: Seed the Public/Authenticated permission matrix programmatically in bootstrap(); add writable-field allowlists to contact-us/responses-career create/update.
  • AC-09: Replace bare strapi::cors with an explicit origin allowlist (alist.ae / www.alist.ae).
  • AC-10: Delete one lockfile, reconcile esbuild-loader in package.json, and enforce npm ci / --frozen-lockfile + Snyk/npm audit gating in CI.
  • AC-13/AC-16: Move uploads off the public web root to an encrypted object store; remove the strapi:strapi DB-credential fallbacks and require a strong DATABASE_PASSWORD.
Hardening
Defense-in-depth and hygiene once the acute items are closed.
  • AC-06: Shorten JWT lifetime (jwt.expiresIn: '1h') with refresh-token rotation or a jti denylist; add iss/aud validation; invalidate tokens on password reset.
  • AC-15: Disable strapi::poweredBy; configure helmet CSP/HSTS/frameguard and add HSTS at the edge.
  • AC-14: Delete the dead src/policies/ratelimit.js and drop the unused express-rate-limit / rate-limiter-flexible packages.
  • AC-17: Force publicationState=live for the Public role to prevent draft disclosure; plan a Strapi 5 migration.

Code quality

Run / deploy

Local setup

# Node 18 or 20 (16 works but is EOL)
yarn install
cp .env.example .env
# replace every "tobemodified*" with a real random value (openssl rand -base64 32)
yarn develop                # http://localhost:1337/admin (autoreload)
# or
yarn build && yarn start   # production-style

Environment

Deployment hints

No deployment artefacts in repo. Frontend alist-website calls https://dev-strapi.alist.ae/ from src/StrapiIntegration/ServerCrenditials.js, implying this CMS was hosted on a single dev box (no prod URL referenced anywhere). Plan a clean rebuild: write a Dockerfile, externalise uploads to S3/Cloudinary via a Strapi upload provider, put DB on a managed MySQL with network ACLs, front the admin panel with a WAF + IP allow-list, and require IMDSv2 on the host.

What to know before editing