Strapi v4 headless CMS that backs the alist-website Vue frontend. Single “Initial commit” from Oct 2023 — stale, unmaintained, internet-reachable.
contact-us, responses-career) that handle job-applicant and contact PII. Pairs with the alist-website Vue 3 frontend, which hard-codes https://dev-strapi.alist.ae/.up_permissions DB table — no code-level policies, no bootstrap seed, no CI gate — so a single admin-UI mistake silently becomes a production exposure that source review cannot disprove..env or secrets committed; admin/JWT secret loading fails-closed (no fallback) which caps cryptographic exposure; all controllers are stock Strapi factories over a parameterized ORM, so no injection sinks were found in custom code. Recovery is mostly “upgrade + lock down permissions + re-host”.| Category | Technology | Version | Notes |
|---|---|---|---|
| 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 |
| Runtime | Node.js | >=16, <=20.x | Node 16 is EOL (Sep 2023); Node 20 still supported |
| Database driver | mysql | 2.18.1 | Legacy callback driver; mysql2 also pulled in indirectly |
| Auth plugin | @strapi/plugin-users-permissions | ^4.14.4 | Standard Strapi auth, JWT-based (HS256, default 30-day expiry) |
| i18n plugin | @strapi/plugin-i18n | ^4.14.4 | Locales feature enabled |
| Upload provider | @strapi/provider-upload-local | ^4.14.4 | Files written to public/uploads/ — served same-origin, not cloud-backed |
| Rate limiting | express-rate-limit, koa2-ratelimit, rate-limiter-flexible | 7.1.0 / 1.1.3 / 3.0.0 | Three libraries pulled in; only koa2-ratelimit is actually wired |
| Admin UI | @strapi/design-system | ^1.11.0 | Default Strapi admin, no customisations |
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.
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.
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-authorizationtesting-jwt-token-security · testing-for-json-web-token-vulnerabilities · exploiting-jwt-algorithm-confusion-attack · testing-api-authentication-weaknessesimplementing-secret-scanning-with-gitleaks · detecting-aws-credential-exposure-with-trufflehog · performing-cryptographic-audit-of-application · testing-for-sensitive-data-exposureexploiting-server-side-request-forgery · testing-cors-misconfiguration · testing-for-host-header-injection · testing-for-open-redirect-vulnerabilitiesperforming-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-prioritizationexploiting-sql-injection-vulnerabilities · exploiting-mass-assignment-in-rest-apis · exploiting-template-injection-vulnerabilitiestesting-for-business-logic-vulnerabilities · exploiting-race-condition-vulnerabilities
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.
| Category | Status | Notes |
|---|---|---|
| A01:2021 Broken Access Control | Vuln | Unauthenticated 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 Failures | Vuln | PII (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 Injection | Clean | All controllers are default Strapi factories over a parameterized ORM; no raw SQL, no shell, no template injection sinks found in src/. |
| A04:2021 Insecure Design | Partial | Append-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 Misconfiguration | Vuln | Wildcard 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 Components | Vuln | Strapi 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 Failures | Vuln | No 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 Failures | Vuln | package.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 Failures | Partial | Only 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 Forgery | Partial | No 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) | Vuln | API1 write-BOLA (AC-01), API2 broken auth throttling + long JWTs (AC-02/AC-06), API3 mass-assignment + draft over-exposure (AC-11/AC-17). |
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.
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.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.
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.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.
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.
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.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.
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.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.
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.
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.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.
>= 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.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.
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.
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.
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.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.
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.
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.
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.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.
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.
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.
'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.Confirmed plugins.js has no jwt block and extensions is empty; stock 30-day HS256 behaviour applies. Valid hardening finding.
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.
.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.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.
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.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.
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.
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.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.
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.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.
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.
fetch('https://dev-strapi.alist.ae/api/...') and reads the response cross-origin because ACAO:* permits it.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.
{ 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.Confirmed bare strapi::cors and no env override. Strapi 4 default ACAO is *. Valid; severity capped by JWT (non-cookie) auth model.
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.
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.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.
^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.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.
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.
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).
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.Confirmed default controllers and no sanitize/allowlist anywhere. Impact is limited because schemas have no privilege fields; kept P3. Closely related to AC-01.
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.
/api/tests; if Public retains the default create grant from dev, POSTs arbitrary records to plant content or probe behaviour.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.
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.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.
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.
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.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.
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.Confirmed schemas have no private flag and the local provider serves from the public root. Genuine hardening/PII finding; kept P3.
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.
Misleads operators into believing global rate limiting exists; contributes to the absence of effective brute-force protection on auth endpoints. No direct exploit.
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.Merged the two duplicate dead-policy findings (auth_jwt + ssrf_misconfig). Confirmed v3 shape and that nothing references it.
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.
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).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.
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.Confirmed both middlewares are bare defaults with no override. Standard hardening finding; P3.
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.
strapi:strapi and dumps all applicant/contact PII.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.
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.Confirmed the literal 'strapi' fallbacks in all three blocks. Low severity, gated on DB network exposure.
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.
GET /api/blogs?publicationState=preview and reads embargoed/draft posts before their intended release.Disclosure of unpublished marketing content, draft blog posts, or not-yet-live page copy to anonymous users, undermining embargoes and editorial workflow confidentiality.
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.Confirmed 30/30 draftAndPublish and default controllers. Theoretical pending live Public read grant; kept as such.
PUT /responses-careers/:id route; grant Public ONLY create on submission collections; remove update/find/findOne/delete.up_permissions table against intent; delete src/api/test/ and confirm no Public/Authenticated grant on api::test.test..env; regenerate with openssl rand -base64 32.interval: { min: 5 }, max: 5, bind it to all /api/auth/* routes keyed per-IP, and add account lockout/backoff.@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).sizeLimit and a server-side MIME/extension allowlist; restrict CV schema allowedTypes to pdf/doc.bootstrap(); add writable-field allowlists to contact-us/responses-career create/update.strapi::cors with an explicit origin allowlist (alist.ae / www.alist.ae).npm ci / --frozen-lockfile + Snyk/npm audit gating in CI.strapi:strapi DB-credential fallbacks and require a strong DATABASE_PASSWORD.jwt.expiresIn: '1h') with refresh-token rotation or a jti denylist; add iss/aud validation; invalidate tokens on password reset.strapi::poweredBy; configure helmet CSP/HSTS/frameguard and add HSTS at the edge.src/policies/ratelimit.js and drop the unused express-rate-limit / rate-limiter-flexible packages.publicationState=live for the Public role to prevent draft disclosure; plan a Strapi 5 migration.createCoreController/Router/Service factories. Two (contact-us, responses-career) deviate intentionally to lock down to write-only with rate limiting.src/policies/ratelimit.js, src/api/test/, three unused npm deps (express-rate-limit, rate-limiter-flexible, esbuild-loader).^4.14.4 (Oct 2023). mysql@2.18.1 is the legacy driver — mysql2 is preferred. express-rate-limit ^7.1.0 is the only dep that has moved on its own.jest, no tests/ folder, no smoke tests.email, biginteger, media); collection names are snake_case. PII fields lack a private:true flag (see AC-13).# 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
HOST (default 0.0.0.0), PORT (default 1337)APP_KEYS — comma-separated list, used for session signingAPI_TOKEN_SALT, ADMIN_JWT_SECRET, TRANSFER_TOKEN_SALT, JWT_SECRET — all required, no fallback (fail-closed, good)DATABASE_CLIENT = mysql | mysql2 | postgres | sqlite (default sqlite, file at .tmp/data.db)DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD (DB user/pass default to strapi/strapi — see AC-16), DATABASE_URL (alternative)DATABASE_SSL + SSL_* options for managed DBspublic/uploads/ (local provider) — needs writable disk and is served same-origin (see AC-08/AC-13)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.
alist-website, not a predecessor. The website is Vue-only and points at dev-strapi.alist.ae; this repo is that backend. Changing schemas here will break the frontend’s response parsing.up_permissions grants in the running admin before assuming any endpoint is or is not reachable.responses-career router exposes POST and PUT (by primary key). The PUT route is an unauthenticated write-IDOR on applicant PII (see AC-01) — do not assume it is safe just because it is "an internal form endpoint".ctx.request.body.email. A submitter who omits or randomises the email field bypasses the per-user portion of the prefix and falls back to IP-only limiting; the global users-permissions limiter is also misconfigured (see AC-02).database/migrations/ is empty). Removing a field from a schema.json can drop the column on next start — back up the DB first.npm ci will fail because package.json and the lockfiles disagree on esbuild-loader (see AC-10).engines when you upgrade Strapi.