PISA Pakistan Community CERT — "Cyber Secure Pakistan Vision 2030" — full role-based audit with disposable + staff users
https://pisa.maahir.io · /admin
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.
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.
pisa-qa-4317188414@uberip.com via mail.tm), registered through full 3-step flow (Pisa / Lahore / YOUTH / PROFESSIONAL). Tested as USER. Mailer verified — raw headers inspected via mail.tm API. OKpisa-staff-2878414244@uberip.com), registered, then promoted to STAFF via admin.user.update audit event. Tested every admin route. PARTIAL — see B1, B4, B8/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.
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:
103.171.122.217, 202.47.37.1, internal trace IDs)admin.user.update events with role-change metadata ({"role":"STAFF"})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
});
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)
Evidence: All return HTTP/2 307 → /login?next=... for unauthenticated visitors.
| Path | Logged-out response |
|---|---|
/first-aid | 307 → /login?next=%2Ffirst-aid |
/scammed | 307 → /login?next=%2Fscammed |
/bank-helplines | 307 → /login?next=%2Fbank-helplines |
/simulations | 307 → /login?next=%2Fsimulations |
/tools | 307 → /login?next=%2Ftools |
/tools/scam-checker | 307 → /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
Evidence: Logged in as Pisa QA Staff. Page rendered fully:
MiniMax-M2.7 @ https://api.minimax.io/v1glm-5.3, qwen3.5in-v3.mailjet.com20000SMTP 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".
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" },
],
}];
},
};
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).
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.
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>;
"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! 👋").
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.
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.
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.
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.
pisa-qa-4317188414@uberip.com), from pisa@maahir.io PISA Pakistan, with the submitted name in body — no recipient substitution, no transport failure.admin.user.update event with {"role":"STAFF"} meta/admin?denied=X/api/admin/report, /api/admin/audit-export both return 403demo@pisa.org.pk, gcsap-demo@pisa.org.pk) shown on login page/api/status uptime correctly reports 1485 seconds (only UI formatter is wrong, B2)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
| Fix | Time | Files |
|---|---|---|
| Hide AI provider/SMTP host from non-admin pages | 1 hr | app/admin/debug/page.tsx |
| Render /status uptime in hh:mm not hours-only | 30 min | app/status/page.tsx |
| Move /first-aid, /scammed, /bank-helplines to public | 2 hr | move auth check below content |
| Add hCaptcha to /register | 4 hr | app/register/page.tsx |
| Filter admin sidebar by permission | 2 hr | app/admin/AdminSidebar.tsx |
| Add CSP + disable x-powered-by | 1 hr | next.config.js |
| Require email verification on register | 4 hr | mail + DB schema |
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./admin/moderation (6 in queue) but no public dashboard.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.