Skip to content

Simpaisa Website Technical Audit

Date: 2026-04-02 Repository: simpaisa1/sp-website (Bitbucket, project SW) Tech Stack: Next.js 15.4.8, React 18, Tailwind CSS 3.4.1, Strapi CMS backend Audited by: CDO Scope: Security, performance, accessibility, dependencies, deployment


Executive Summary

The sp-website is a modern Next.js 15 application with reasonable infrastructure (Jenkins CI/CD, AWS deployment, Tailwind CSS) but significant gaps in security hardening, performance optimisation, and accessibility compliance. The most critical finding is a severely incomplete Content Security Policy that provides almost no XSS protection, combined with 26 instances of dangerouslySetInnerHTML across the codebase. The contact form lacks CSRF protection, rate limiting, and CAPTCHA. Bundle size is inflated by Monaco Editor (~2MB) which serves no clear purpose on a corporate website.

Security: 4/10 | Performance: 5/10 | Accessibility: 3/10 | Dependencies: 4/10


1. Security

1.1 CRITICAL: Content Security Policy is Non-Functional

File: next.config.mjs:29-36

Current CSP:

frame-src 'none'; font-src * data:; object-src 'none'; base-uri 'self'; form-action 'self';

Missing directives: - default-src — without this fallback, most resource types are unrestricted - script-src — allows any script to execute, negating XSS protection entirely - style-src — allows any stylesheet injection - img-src — allows image loading from any origin - connect-src — allows fetch/XHR to any origin

Additionally: font-src * allows loading fonts from any origin.

Recommended CSP:

default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://snap.licdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https://wb-backend.simpaisa.com https://www.google-analytics.com https://ipinfo.io; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';

Effort: 2-3 hours (requires testing all pages to ensure nothing breaks).

1.2 CRITICAL: Contact Form Has No Server-Side Protection

File: components/ContactUsForm/ContactUsForm.js

Issues: - No CSRF protection — no CSRF token generated or validated. Form submits directly to Strapi API - No rate limiting — form can be submitted unlimited times - No CAPTCHA/reCAPTCHA — no bot protection - No honeypot field — no anti-spam mechanism - Client-side-only validation — all validation (email regex line 187, phone regex line 188, URL regex line 189, sanitisation lines 480-491) is trivially bypassable - Weak HTML sanitisationsanitizeInput() at lines 480-491 uses regex to strip <script> tags — this is bypassable. Use DOMPurify or server-side sanitisation

File: components/EmailSend/EmailSend.js:11 - Posts directly to ${BASE_URL}/api/contact-form-entries — exposes the Strapi API endpoint publicly

Fix: Create a Next.js API route (/app/api/contact/route.js) as a proxy. Add rate limiting, CSRF token, honeypot field, and server-side validation. Add reCAPTCHA v3 or Cloudflare Turnstile.

Effort: 1-2 days.

1.3 HIGH: Image Remote Patterns Wildcard

File: next.config.mjs:9

{ protocol: "https", hostname: "**" }

Allows loading remote images from any HTTPS host via the Next.js image optimisation proxy. This could be exploited for SSRF (Server-Side Request Forgery) — an attacker could craft URLs that cause the server to make requests to internal resources.

Fix: Restrict to known domains:

{ protocol: "https", hostname: "wb-backend.simpaisa.com" },
{ protocol: "https", hostname: "*.simpaisa.com" },

Effort: 15 minutes.

1.4 HIGH: .env Not Gitignored

File: .gitignore:29

Only .env*.local is excluded. The base .env file is committed to the repository. While it currently contains only a public URL (NEXT_PUBLIC_BASEURL), this is a bad practice — developers may add secrets to .env in future without realising it is tracked.

Fix: Add .env to .gitignore. Move the current value to .env.example (committed) and .env (gitignored).

Effort: 15 minutes.

1.5 HIGH: dangerouslySetInnerHTML Usage

26 instances across 12 component files. While data currently comes from hardcoded content, some components receive data from the Strapi CMS API, creating XSS risk if CMS content is compromised.

Files affected: - components/TwoCol/TwoCol.js:133,138,145 - components/BlackTabSection/BlackTabSection.js:199 - components/leadership/leadership.js:56 - components/CoveragePayments/FadeSlider.js:23,27 - components/BlogDetailComponent/BlogDetailComponent.js (CMS content) - components/NewsRoomDetail/NewsRoomDetail.js (CMS content) - Plus 6 additional components

Fix: For CMS-sourced content, sanitise with DOMPurify before rendering. For hardcoded content, consider using JSX instead.

Effort: 4-6 hours.

1.6 MEDIUM: Missing Security Headers

Header Status Recommended Value
X-DNS-Prefetch-Control Missing off
Cross-Origin-Opener-Policy Missing same-origin
Cross-Origin-Resource-Policy Missing same-origin
Cross-Origin-Embedder-Policy Missing require-corp
X-Permitted-Cross-Domain-Policies Missing none

Effort: 30 minutes (add to next.config.mjs headers function).

1.7 MEDIUM: Third-Party Data Leakage

File: components/ContactUsForm/ContactUsForm.js:194

The form calls https://ipinfo.io/json for geolocation without an API token (free tier: 50K req/month) and without user consent. This leaks the user's IP address to a third-party service.

Fix: Remove ipinfo.io call or add explicit consent. If geolocation is needed, use the browser's Geolocation API with user permission.

Effort: 30 minutes.

1.8 LOW: GA Measurement ID Hardcoded

File: app/layout.js:67

G-54PEFW0LDQ is hardcoded rather than using an environment variable. This makes it harder to use different GA properties per environment (dev/qa/uat/prod).

Fix: Move to NEXT_PUBLIC_GA_ID environment variable.

Effort: 15 minutes.


2. Performance

2.1 HIGH: Monaco Editor in Bundle (~2MB)

File: package.json:12@monaco-editor/react: ^4.6.0 Used in: components/CodeSnippet/codesnippet.js:5

Monaco Editor is VS Code's full code editor engine. It adds approximately 2MB to the client bundle. On a corporate website, this is used to display code snippets — a task that a lightweight syntax highlighter like prism-react-renderer (~6KB) handles perfectly.

Fix: Replace @monaco-editor/react with prism-react-renderer or react-syntax-highlighter.

Effort: 2-3 hours.

2.2 HIGH: All 9 Font Weights Loaded

File: app/layout.js:12

Poppins is loaded with all 9 weights (100-900). Each weight adds ~10-15KB. A typical site needs 3-4 weights.

Fix: Reduce to weights actually used: 300, 400, 600, 700.

Effort: 15 minutes.

File: package.json

Two separate carousel libraries are installed: - react-slick (line 33) + slick-carousel (line 36) — jQuery-based, unmaintained since 2017 - swiper (line 37) — modern, actively maintained

Fix: Consolidate to Swiper only. Remove react-slick and slick-carousel.

Effort: 1-2 days (requires migrating all slider components).

2.4 MEDIUM: Duplicate Analytics Packages

File: package.json

  • nextjs-google-analytics (line 26) is installed but GA is implemented manually in layout.js via raw script tags
  • nextjs-linkedin-insight-tag (line 27) is installed but LinkedIn tracking is implemented manually in components/LinkedInAnalytics/

Fix: Remove the unused packages: nextjs-google-analytics and nextjs-linkedin-insight-tag. Or migrate to use them properly and remove the manual implementations.

Effort: 30 minutes.

2.5 MEDIUM: GA Script Loading Strategy

File: app/layout.js:73-83

Google Analytics is loaded via raw <script> tags with dangerouslySetInnerHTML inside a client component. Next.js provides the <Script> component with optimised loading strategies (afterInteractive, lazyOnload).

Fix: Use Next.js <Script strategy="afterInteractive"> for GA.

Effort: 30 minutes.

2.6 LOW: Three.js Bundle (~150KB)

File: package.json:39-40three + three-globe

The 3D globe component adds ~200KB to the bundle. It is already loaded with ssr: false (good), but consider whether the globe is essential for the corporate website or if a static SVG map would suffice.

Effort: Evaluation only — no change unless product decision is made.


3. Accessibility

No skip navigation link exists anywhere in the codebase. This is a WCAG 2.1 Level A failure (Success Criterion 2.4.1). Keyboard users must tab through the entire header navigation on every page before reaching main content.

Fix: Add <a href="#main-content" class="sr-only focus:not-sr-only">Skip to main content</a> as the first element in the body, and id="main-content" on the <main> element.

Effort: 30 minutes.

3.2 HIGH: 73+ Images with Missing or Non-Descriptive Alt Text

Breakdown: - 10 instances of alt="" (empty — screen readers skip entirely) - 63 instances of alt="img" (non-descriptive placeholder) - 1+ instances of missing alt attribute entirely (app/product/page.js:24)

WCAG 2.1 Level A failure (Success Criterion 1.1.1). Every meaningful image must have descriptive alt text.

Files with worst offences: - components/CoverageExcellence/CoverageExcellence.js — 5× alt="img" - components/SecuritySection/SecuritySection.js — 4× alt="img" - components/TrustSection/TrustSection.js — 4× alt="img" - app/page.js — 3× alt="img"

Effort: 2-3 hours (requires writing meaningful descriptions for each image).

3.3 MEDIUM: Form Labels Disconnected

File: components/ContactUsForm/ContactUsForm.js

Several form <label> elements have their htmlFor attribute commented out, breaking the programmatic association between label and form control: - Line 724: jobFunction label - Line 776: jobLevel label - Line 1069: legalStructure label - Line 1143: operatingSince label - Line 1410: productInterestedIn label - Line 1534: another label

WCAG 2.1 Level A failure (Success Criterion 1.3.1).

Fix: Uncomment the htmlFor attributes.

Effort: 15 minutes.

3.4 MEDIUM: Non-Interactive Elements with Click Handlers

File: components/ContactUsForm/CustomMultiSelect .js:90onClick on a <div> without role or tabIndex. Keyboard users cannot interact with this element.

Fix: Add role="button", tabIndex="0", and onKeyDown handler.

Effort: 15 minutes.

3.5 MEDIUM: Minimal ARIA Coverage

Only 25 aria-* attributes across 10 component files for a site with 90+ components. Key interactive elements (sliders, modal-like sections, expandable content) likely lack proper ARIA labelling.

Effort: 4-6 hours for a comprehensive ARIA audit and remediation.

3.6 LOW: Invalid Nested HTML

File: components/StickySection/StickySection.js:130-178

<p> tags nested inside other <p> tags — invalid HTML per the spec. Browsers may render unpredictably.

Fix: Replace inner <p> tags with <span> or restructure the markup.

Effort: 30 minutes.


4. Dependencies

File: package.json:36slick-carousel: ^1.8.1

Last maintained in 2017. Has a known XSS vulnerability via its jQuery dependency. Should be replaced as part of the carousel consolidation (see 2.3).

4.2 MEDIUM: eslint-config-next Version Mismatch

File: package.json:45eslint-config-next: 14.2.5

The ESLint config is pinned to Next.js 14 while the application runs Next.js 15. These should match major versions.

Fix: npm install --save-dev eslint-config-next@latest

Effort: 15 minutes.

4.3 MEDIUM: @tailwindcss/line-clamp Deprecated

File: package.json:14@tailwindcss/line-clamp: ^0.4.4

This plugin is deprecated — line-clamp is built into Tailwind CSS v3.3+ natively.

Fix: Remove the plugin from package.json and tailwind.config.js. Replace any line-clamp-* plugin classes with native Tailwind line-clamp-* utilities (same class names, no plugin needed).

Effort: 30 minutes.

4.4 LOW: cross-spawn Potential CVE

File: package.json:16cross-spawn: ^7.0.6

CVE-2024-21538 (ReDoS) affects versions prior to 7.0.5. Version ^7.0.6 should be safe, but verify the resolved version in package-lock.json.

4.5 LOW: Unused Dependencies

  • nextjs-google-analytics — manually implemented
  • nextjs-linkedin-insight-tag — manually implemented
  • file-saver — verify if still used

Effort: 15 minutes to audit and remove.


5. Deployment & Infrastructure

5.1 MEDIUM: Single Credential Across All Environments

Files: devops/JenkinsFile_DEV, _QA, _UAT, _PROD

All four environment pipelines use the same credentialsId: 'prod-bitbucket-ap'. Best practice is separate credentials per environment to limit blast radius if compromised.

Fix: Create per-environment credentials in Jenkins credential store.

Effort: 1 hour.

5.2 LOW: No Dockerfile in Repository

Build and containerisation configuration is externalised to a separate deployment_config repository. While this separates concerns, it means the application cannot be built standalone from this repository alone.

Note: This is an architectural decision, not necessarily a problem.


6. i18n

6.1 MEDIUM: Non-Functional i18n Setup

File: package.json:23next-intl: ^3.26.5

next-intl is installed but: - No i18n.js or i18n.ts configuration file exists - No middleware.js for locale routing - /locales/en.json and fr.json contain only 2 placeholder keys (greeting, welcome) - The site uses Google Translate widget instead (imported in components/header/header.js:8 and components/Footer/Footer.js:8) - Root <html> hardcodes lang="en" (app/layout.js:69)

Impact: No locale-specific URLs, no hreflang tags, machine-translated content is poor for SEO. Google may index machine-translated pages with incorrect language signals.

Fix: Either implement next-intl properly with locale-prefixed routes and hreflang tags, or remove the dependency and formalise the Google Translate approach with a <meta name="google" content="notranslate"> for pages that should not be auto-translated.

Effort: Depends on scope — full i18n: 2-4 weeks; cleanup only: 30 minutes.


Priority Summary

P0 — Critical (fix this week)

  1. Complete the CSP with default-src, script-src, style-src, img-src, connect-src (1.1)
  2. Add CSRF/rate limiting/CAPTCHA to contact form (1.2)
  3. Restrict image remote patterns from ** wildcard (1.3)

P1 — High (fix this sprint)

  1. Add .env to .gitignore (1.4)
  2. Replace Monaco Editor with lightweight alternative (2.1)
  3. Reduce font weights from 9 to 3-4 (2.2)
  4. Add skip navigation link (3.1)
  5. Fix 73+ image alt texts (3.2)
  6. Replace unmaintained slick-carousel (4.1)

P2 — Medium (fix this month)

  1. Add missing security headers (1.6)
  2. Sanitise dangerouslySetInnerHTML for CMS content (1.5)
  3. Consolidate duplicate carousel libraries (2.3)
  4. Remove duplicate analytics packages (2.4)
  5. Fix disconnected form labels (3.3)
  6. Update eslint-config-next to v15 (4.2)
  7. Remove deprecated @tailwindcss/line-clamp (4.3)
  8. Remove or properly implement i18n (6.1)

P3 — Low (backlog)

  1. Remove ipinfo.io call or add consent (1.7)
  2. Move GA ID to environment variable (1.8)
  3. Evaluate Three.js necessity (2.6)
  4. Fix nested <p> tags (3.6)
  5. Remove unused dependencies (4.5)
  6. Create per-environment Jenkins credentials (5.1)

Cross-References

  • SEO/AEO issues → See AEO/SEO-AEO-AUDIT-REPORT.md (updated 2026-04-02 with code-level verification addendum)
  • API issues → See API/API-Best-Practices-Audit.md, API/GitBook-API-Audit.md, API/OpenBanking-Comparison-Audit.md
  • Architecture overview → See Architecture/Standards/ARCHITECTURAL-REVIEW.md