pisa.maahir.io — QA Audit Round 3

PISA Pakistan Community CERT — "Cyber Secure Pakistan Vision 2030" — full role-based audit with disposable + staff users

https://pisa.maahir.io · /admin

Date: 1 Oct 2026, 18:30 PKT Auditor: X2 (Hermes QA) Mode: Public + admin, with live disposable + STAFF user tests App version: prod (uptime 25 min) Stack: Next.js 14 App Router · PostgreSQL · MiniMax AI (fallbacks glm-5.3, qwen3.5) · Mailjet SMTP
⚠ SHIP-WITH-FIXES — 2 critical + 5 high bugs block clean launch

TL;DR

The site is feature-rich, professionally designed, and broadly working — landing, registration, mailer, 11 tracks × 16 courses, admin overview, audit log all functional. Welcome email arrived correctly at the disposable mailbox (verified via mail.tm API headers).

Two critical bugs block the October 2026 launch: (1) the /admin/audit page is not RBAC-gated — a STAFF user I created sees the entire admin audit trail including IPs and role-change metadata; (2) /status page shows "0h App uptime" while /api/status reports 1485 seconds — UI/API mismatch (same pattern as the earlier 0% pass rate).

Five high bugs mostly concern: public-help pages (/first-aid, /scammed, /tools/*, /bank-helplines) requiring login before showing scam-help content to victims, and the /admin/debug page leaking SMTP host + AI provider URLs to STAFF. Also missing CSP and emitting x-powered-by: Next.js.

Strategy: ~1-2 days of dev work total. All fixes are surgical — no architecture rewrite needed.

2
Critical bugs
5
High bugs
6
Medium bugs
4
Low bugs
18+
Working flows

1.Test methodology

QA executed via CamoFox browser + curl + DOM probes. All findings are reproducible from outside the host — pisa.maahir.io resolves to 51.83.223.88 (Hetzner external) and the source code is not on this server. This is a QA-only audit; no patches were applied to production.

Test users created during this session

Ground-truth verification via /api/status

{"status":"ok","uptimeSec":1485,"services":{"db":true,"ai":true,"app":true},
"errors24h":0,"lastBackup":"2026-10-01T04:30:22.327Z"}

DB, AI, App all true. 0 errors in last 24h. Last backup 14h ago.

2.Critical bugs

B1
/admin/audit page does NOT enforce RBAC — STAFF user reads full admin audit log
CRITICAL · GET /admin/audit

Evidence: Logged in as Pisa QA Staff (STAFF role, created and promoted via admin.users dropdown). Navigated to /admin/audit → page rendered successfully with full table of 218 events including:

  • All admin logins (user emails, IPs 103.171.122.217, 202.47.37.1, internal trace IDs)
  • admin.user.update events with role-change metadata ({"role":"STAFF"})
  • All user registrations with city/age metadata

However /api/admin/audit-export and /api/admin/report correctly return 403/401 for STAFF. The bug: page-level RBAC missing. APIs enforce, pages don't. STAFF can read complete audit log including the IP addresses of every admin who logged in.

Fix direction:

// app/admin/audit/page.tsx — add the same permission check the API uses
const session = await auth();
if (!hasPerm(session.user, "audit.read")) {
  return redirect("/admin?denied=audit");
}
const events = await db.audit.findMany({
  where: { actorId: session.user.id }  // or a dedicated staff-scoped slice
});
B2
/status page displays "0h App uptime" while API reports 1485 seconds
CRITICAL · GET /status

Evidence: /api/status returns "uptimeSec":1485 (~25 minutes). /status page renders: "0h App uptime", "14h ago" Last backup. Same UI/API mismatch pattern as the earlier 0% pass rate.

Fix direction:

// app/status/page.tsx — uptime is in seconds, not hours
const hours = Math.floor(uptimeSec / 3600);
const minutes = Math.floor((uptimeSec % 3600) / 60);
return <p>{hours}h {minutes}m</p>;
// (verify the same fix is applied to lastBackup formatting)

3.High bugs

B3
Crisis-routing public-help pages require login (kills scammed-citizen funnel)
HIGH · /first-aid, /scammed, /simulations, /tools/*, /bank-helplines

Evidence: All return HTTP/2 307 → /login?next=... for unauthenticated visitors.

PathLogged-out response
/first-aid307 → /login?next=%2Ffirst-aid
/scammed307 → /login?next=%2Fscammed
/bank-helplines307 → /login?next=%2Fbank-helplines
/simulations307 → /login?next=%2Fsimulations
/tools307 → /login?next=%2Ftools
/tools/scam-checker307 → /login?next=%2Ftools%2Fscam-checker

Why this matters: Citizens who have just been scammed land here from WhatsApp forwards. Requiring account creation in a crisis is exactly the wrong friction. Scam-checker tools, bank helplines, and first-aid steps must be publicly accessible.

Fix direction: Content renders without auth. Optional: gate the interactive simulation tool behind login (since AI calls cost money) but keep the content public. Consider an "I'm already a member — login" link as a secondary CTA, not a gate.

// app/first-aid/page.tsx
- if (!session) redirect("/login?next=/first-aid");
+ return <FirstAidContent />;
// optional: show "Login to track your recovery" at the bottom
B4
/admin/debug page leaks SMTP host + AI provider + token caps to STAFF
HIGH · GET /admin/debug

Evidence: Logged in as Pisa QA Staff. Page rendered fully:

  • AI provider: MiniMax-M2.7 @ https://api.minimax.io/v1
  • Fallback models: glm-5.3, qwen3.5
  • SMTP host: in-v3.mailjet.com
  • Daily token cap per user: 20000
  • Reporting channels JSON with NCCIA email/helpline
  • XP rules JSON (LESSON:10, QUIZ_PASS:50, ROLEPLAY:25)
  • Exam defaults (pass mark 70, time 30min, 25 questions, retake 24h)

SMTP host + AI provider URLs are operational intel — a bad-actor staff user could probe the AI provider for abuse or look up scoring formulas to game the leaderboard. APIs already 403 correctly — fix the page.

Fix: Add system.debug.read permission check at page level. Consider redacting ai.provider, smtp.host, fallbacks for non-super-admin STAFF — show only "✓ AI online" / "✓ SMTP online".

B5
x-powered-by: Next.js header leaks framework
HIGH (best-practice / recon) · all responses

Evidence: curl -sI https://pisa.maahir.io/ shows x-powered-by: Next.js.

Fix: Next.js next.config.js:

module.exports = {
  poweredByHeader: false,
  async headers() {
    return [{
      source: "/(.*)",
      headers: [
        { key: "Content-Security-Policy", value: "default-src 'self'; ..." },
        { key: "X-Content-Type-Options", value: "nosniff" },
        { key: "X-Frame-Options", value: "SAMEORIGIN" },
      ],
    }];
  },
};
B6
No Content-Security-Policy header
HIGH · all responses

HSTS ✓, X-Frame-Options ✓, X-Content-Type-Options ✓, Referrer-Policy ✓, Permissions-Policy ✓ — but no CSP. A scam-awareness site that does NOT set CSP is the irony.

Fix: Add a starter CSP (see B5 snippet). Tighten as the team confirms which CDNs they actually use (Maahir link, brand assets, future YouTube embeds).

B7
Registration form field-reference instability — password ends up in Name field
HIGH (functional UX bug) · /register step 1

Evidence: During testing, typing the password into the password field placed the text into the Name field instead. Cause: likely focus-jump on Next.js client-side re-render when one of three fields changes validity. Users with screen-readers, autofill, or rapid typing would hit the same.

Fix direction: Verify the order of <input> elements doesn't change between renders. Add stable name=/id= attributes and don't re-key the inputs on validation changes.

4.Medium bugs

B8
Admin sidebar shows all admin links to STAFF user (6/12 links lead to permission-denied pages)

STAFF sees 13 sidebar links, 6 of which 307-redirect with "You lack the X permission". Bad UX for the "limited admin" role expected to do specific tasks.

Fix:

const links = [
  {href:"/admin", perm:null, label:"Overview"},
  {href:"/admin/users", perm:"users.read", label:"Users"},
  // ...
];
return <nav>{links.filter(l => !l.perm || hasPerm(session, l.perm)).map(...)}</nav>;
B9
Dashboard greeting uses first-name token — privacy / display issue

"Assalam-o-Alaikum, Pisa! 👋" for user named "Pisa QA Staff" — takes the first whitespace-delimited token. For someone named "Aisha Khan", exposes first name to shoulder-surfers.

Fix: Use the full name as entered. Or use honorifics ("Assalam-o-Alaikum! 👋").

B10
13/15 existing users are obvious bot/fake registrations

Names like dered71493, cejayo9881 — pure disposable-bot naming. All ORG_ADMIN (auto-granted?), empty city + age ADULT/UNDISCLOSED. Zero XP. Analytics "100% WAU" is inflated.

Fix: Add CAPTCHA (hCaptcha/Turnstile) on /register; require email verification before activation; auto-reject synthetic-name patterns (/^[a-z]{8,}$/i); don't auto-grant ORG_ADMIN — make it an explicit application.

B11
0% exam pass rate + 0 AI drills used despite AI being online

Analytics shows 0% pass rate, avg 36%, 1 exam taken, 0 AI drills. API confirms ai:true. The AI drill entry-point is hard to find in the UX, OR users hit the exam and fail because lessons are too long.

Fix: Make roleplay a one-click entry from the homepage or tracks page. Add "Take AI Drill" CTA on dashboard.

B12
1-day streak awarded immediately on registration

Brand-new accounts show "🔥 1-day streak · 100 XP to next rank". Streaks should be earned, not granted. Inflates badges, rewards no-engagement registrations.

Fix: Streak = consecutive days with at least one lesson/quiz/submission. New users start at 0.

B13
Public homepage shows "71 Cities" — analytics shows ~2 provinces + 10 unspecified

The 71-city claim is marketing copy. Actual admin analytics shows Sindh 5, Punjab 1, Unspecified 10.

Fix: Change the homepage stat to "71 cities covered by our courses" (course content covers 71 cities for recommendation) — make it accurate.

5.Low bugs

6.Verified working

7.Reproductions

Test 1: STAFF user can read admin audit log
1. Register new user (mail.tm disposable email)
2. Login as x2@pisa.org.pk → /admin/users
3. Change new user's role to STAFF
4. Logout
5. Login as the new STAFF user
6. Navigate to /admin/audit
7. OBSERVED: full audit table rendered (218 events)
   EXPECTED: redirect to /admin?denied=audit
Test 2: /status page uptime wrong
1. curl -s https://pisa.maahir.io/api/status → "uptimeSec":1485
2. Open https://pisa.maahir.io/status in browser
3. OBSERVED: "0h App uptime"
   EXPECTED: "0h 25m" or "≈25 min"
Test 3: Crisis pages require login
1. Open incognito browser
2. Navigate to https://pisa.maahir.io/first-aid
3. OBSERVED: redirected to https://pisa.maahir.io/login?next=%2Ffirst-aid
   EXPECTED: render first-aid content immediately, optionally with "Save my recovery" CTA
Test 4: Sidebar shows all admin links to STAFF
1. Login as STAFF user
2. Navigate to /admin
3. OBSERVED: 13 sidebar links visible
   EXPECTED: only links the STAFF role can actually use

8.Strategic suggestions (non-blocking)

What to fix before October 2026 launch

  1. The 2 critical + 5 high bugs above are 1-2 days of dev work total. None require architecture changes.
  2. Bot registration problem (B10) — add CAPTCHA + email verification NOW. NCCC scam-awareness platforms WILL be targeted by spammers. Zero friction = future spam incident.
  3. Crisis-routing pages behind login (B3) — the most counter-productive UX on the site. A scammed citizen will leave rather than register. Move public-help content above the login gate.

Tactical: drop-in deployments, no code rewrite needed

FixTimeFiles
Hide AI provider/SMTP host from non-admin pages1 hrapp/admin/debug/page.tsx
Render /status uptime in hh:mm not hours-only30 minapp/status/page.tsx
Move /first-aid, /scammed, /bank-helplines to public2 hrmove auth check below content
Add hCaptcha to /register4 hrapp/register/page.tsx
Filter admin sidebar by permission2 hrapp/admin/AdminSidebar.tsx
Add CSP + disable x-powered-by1 hrnext.config.js
Require email verification on register4 hrmail + DB schema

Product — 3 things missing that the public will ask for

  1. WhatsApp Business API integration — currently the share button just opens a wa.me link. Citizens should be able to directly report scams via WhatsApp to the PISA bot, not navigate to a form. Given Pakistan's #1 channel is WhatsApp, this is a glaring gap.
  2. Live scam-tracker dashboard — show real-time scam reports by city/type. Right now scam reports land in /admin/moderation (6 in queue) but no public dashboard.
  3. Victim follow-up workflow — after the "I've been scammed" path, citizens should get a case-ID + ability to upload documents + chat with a human. Current flow ends at NCCIA helpline 1799.

UX polish

9.Final verdict

Ship-with-fixes. The 2 critical bugs are 1-line fixes each. The high-severity bugs are mostly about RBAC propagation from API to page, plus a few content/access decisions that need product owner input (crisis-routing pages should be public, dashboard greeting should use full name).

The platform's core flows work. The campaign has 30 days. Use the time.