Pakistan's anonymous school accountability platform · PHP/FrankenPHP · MariaDB
Working: Guest one-click login for 6 demo roles · real login with CSRF · complaint filing end-to-end · vouch threshold (3+) correctly toggles reveal flag · token-gated complaint views · showcase school has full 30-facility checklist · /admin shows pending schools/users/purge · XSS probes escaped properly · /api/schools.php returns clean JSON.
Critical: "Decoy password" promised as the #1 safety feature is completely fictional — no second password field exists on register, login, or onboarding. All school links from /dashboard 404 (use ?slug= but page wants ?id=). Zero security headers (no HSTS/CSP/X-Frame-Options) on a platform whose entire pitch is student anonymity.
Data hygiene: Debug seeds ("Debug21787606713", "FinalTest1787606720") publicly ranked alongside real schools. 5 of 8 geocoded schools have lat=0,lon=0 (null island). Forum has 7 categories, 0 threads.
All 6 demo accounts login successfully. Real password login works with CSRF. Logout clears session. /onboarding redirects on first login.
| Role | Username | Password | Login Method | Result |
|---|---|---|---|---|
| Student | demo_student | Demo123! | Guest link / Form | OK |
| Teacher | demo_teacher | Demo123! | Guest link / Form | OK |
| Headmaster | demo_headmaster | Demo123! | Guest link / Form | OK |
| School Admin | demo_schooladmin | Demo123! | Guest link / Form | OK |
| Govt / NGO | demo_govt | Demo123! | Guest link / Form | OK |
| Super Admin | demo_admin | Demo123! | Guest link / Form | OK |
| Logout | — | — | GET /auth/logout | OK — redirects to /auth/login |
| Decoy password | — | — | Search entire codebase | NOT IMPLEMENTED — see finding #1 |
| Method | Route | Code | Result |
|---|---|---|---|
| GET | / | 200 | Homepage loads — hero, 20+ categories, "concept" explainer, finder, all 5-step flow |
| GET | /dashboard | 200 | Public dashboard with live counters, recent feed, rankings, heatmap, showcase |
| GET | /schools | 200 | Schools list — but most lack lat/lon, debug seeds visible |
| GET | /schools/finder.php | 200 | Leaflet + OSM map, search by name/city/district |
| GET | /schools/view?slug=test-school-d44f6b | 404 | BROKEN — page wants ?id= not ?slug= (see finding #2) |
| GET | /schools/view?id=14 | 200 | Showcase school profile with full 30-facility checklist, safety score 78/100, rating 4.2 |
| GET | /complaints | 200 | List with filter UI; pagination links present |
| GET | /complaints/view?token=... | 200 | Token-gated views; fake token returns 404 |
| GET | /complaints/view?token=aaaa | 404 | Not-found path — tokens not guessable |
| GET | /forum | 200 | 7 categories all showing "0 threads" — empty frame |
| GET | /roadmap | 200 | Claims 210+ features, 13 phases, 7 roles — text-only |
| GET | /auth/login | 200 | One password field — NO decoy (see finding #1) |
| GET | /auth/register | 200 | One password field — NO decoy (see finding #1) |
| GET | /profile | 200 (homepage) | Route does not exist — falls through to homepage |
| GET | /settings | 200 (homepage) | Route does not exist — falls through to homepage |
Out of 8 common API paths probed, only /api/schools.php exists. Everything else falls through to homepage.
| Endpoint | Status | Notes |
|---|---|---|
| /api/schools.php | 200 JSON | Returns full school list with province/district joins. 37 total, 8 with lat/lon (5 at null island 0,0) |
| /api/vouch | 200 JSON | POST {complaint_id, action} → {vouched, vouch_count, reveal}. Threshold at 3+ toggles reveal=1 |
| /api/vouch.php | 200 JSON | Exposed but no complaint_id validation |
| /api/provinces.php | HTML 200 | Returns homepage — endpoint does not exist |
| /api/categories.php | HTML 200 | Returns homepage — categories hardcoded in form |
| /api/districts.php | HTML 200 | Returns homepage |
| /api/complaints.php | HTML 200 | Returns homepage |
| /api/stats.php | HTML 200 | Returns homepage |
| /openapi.json | HTML 200 | Returns homepage |
| /healthz | HTML 200 | Returns homepage |
| Workflow | Outcome | Notes |
|---|---|---|
| File a complaint | WORKS | CSRF + multipart → token returned, count incremented 20→21, appears on dashboard feed |
| Vouch / Confirm | WORKS | Each role can vouch once; vouch_count increments; reveal flag flips at 3+ |
| Teacher name reveal at 3+ vouches | PARTIAL | Flag toggles correctly but showcase complaints have no teacher_name field, so nothing actually displays |
| Anonymity — "Your name is never stored with complaint" | VERIFIED | Complaint view shows "Anonymous" + random handle (e.g. SharpWolf966), no email/username leaked |
| Token-based share link | WORKS | 32-char hex token, 404 on invalid; shareable via direct URL |
| Admin school verification | WORKS | /admin shows pending schools with Approve/Reject buttons |
| Demo data purge | WORKS | POST /admin/purge.php cleaned my QA test complaints; counts back to 7/12/5 |
| Audio evidence (WebM) | BROKEN | audio_path is a free-text hidden field — server accepts arbitrary paths including /etc/passwd (see finding #4) |
| AI auto-tag / sentiment / correlation | UNVERIFIED | No AI endpoint exposed; "73% correlation" claim from homepage not testable |
Probed /, /auth/login, /dashboard, /complaints. Only Server + X-Powered-By returned. None of the expected security headers present.
| Header | Expected | Found |
|---|---|---|
Strict-Transport-Security | Required for TLS enforcement | MISSING |
Content-Security-Policy | Mitigates XSS, third-party script injection | MISSING |
X-Frame-Options | Clickjacking protection | MISSING |
X-Content-Type-Options | MIME sniffing protection | MISSING |
Referrer-Policy | Privacy on outbound links | MISSING |
Permissions-Policy | Browser feature lockdown | MISSING |
X-Powered-By | Should be stripped | LEAKED — PHP/8.5.9 |
| # | Bug | Endpoint | Severity | Repro |
|---|---|---|---|---|
| 1 | Decoy password is marketing fiction. Homepage hero sells it as the #1 safety promise ("Forced to login? Give your decoy password — opens empty view") but no second password field exists on /auth/register, /auth/login, /onboarding, or any other route. A coerced user has only one password to give up, and giving it up exposes everything. | /auth/login/auth/register/onboarding |
CRITICAL | 100% |
| 2 | All school "View" links from /dashboard 404. Dashboard renders ?slug=test-school-d44f6b but /schools/view expects ?id=N. Every ranked school link is broken. |
/schools/view |
CRITICAL | 100% (6/6 dashboard links tested) |
| 3 | Zero security headers on all routes. No HSTS to enforce TLS, no CSP to limit XSS, no X-Frame-Options. For a platform whose entire pitch is whistleblower anonymity, this is an unacceptable baseline. | Global | CRITICAL | 100% (4/4 routes tested) |
| 4 | Audio upload path is unverified. /complaints/new accepts audio_path as a free-text hidden field. Successfully filed a complaint with audio_path=/etc/passwd and audio_duration=99999 — both accepted without validation. If server tries to read it, LFI risk; if just stored, the feature is a facade. |
POST /complaints/new |
CRITICAL | 100% |
| 5 | Heatmap data is mostly null. 5 of 8 geocoded schools have lat=0, lon=0 (null island in Atlantic). Real Karachi/Lahore schools cluster with debug seeds at 0,0. | /api/schools.php |
MEDIUM | 5/8 records |
| 6 | Debug seeds publicly ranked. School names like "Debug21787606713", "FinalTest1787606720", "Test Student School 1787606679" visible in /schools and /dashboard rankings alongside real schools. | /schools, /dashboard | MEDIUM | 5+ records |
| 7 | Forum is empty. All 7 categories show "0 threads" — feature is a frame with no content. | /forum | MEDIUM | 7/7 categories |
| 8 | Demo accounts live in production. demo_student / Demo123! publicly documented on /auth/login, works against live server. | /auth/login | MEDIUM | 6/6 demo logins |
| 9 | /profile, /settings, /account return homepage — no actual route exists. | Global | LOW | 3/3 routes |
| Claim | Source | Status |
|---|---|---|
| "210+ features, 13 phases, 7 roles" | Homepage + /roadmap | Text-only — only 6 demo roles exist; 7th unspecified |
| "500+ Metrics, 15+ charts" | Homepage | Counts (7/12/5) don't justify 500+ metrics; charts not enumerated |
| "AI finds 73% correlation: salary-delay → teacher absent" | Homepage | No AI endpoint exposed; claim unprovable in current build |
| "5 STT Langs (Urdu, Punjabi, Sindhi, Pashto, English)" | Homepage hero stats | No STT tested; transcript box shows "Transcript placeholder — will be generated" |
| "WhatsApp · SMS · USSD *123# · IVR" | Homepage + /roadmap | Only web interface exists; no channel integration visible |
| "Province → UC, BISE & SEMIS" | Homepage | Province → district works; UC level not surfaced |
| "QR on every gate → profile" | Homepage footer | No QR generation endpoint probed |
password_hash_decoy column, separate Argon2id hash, login flow that checks real first then decoy (returns empty dashboard), and an onboarding step to set it. If that's not shipping in this milestone, strip every reference to "decoy password" and "Forced to login?" from the homepage and roadmap. False safety promises on a whistleblower platform are worse than no claim.Strict-Transport-Security: max-age=31536000; includeSubDomains, Content-Security-Policy: default-src 'self', X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer. Strip X-Powered-By./api/audio-upload.php that returns a UUID mapping to a temp path the server controls. Reject anything that looks like /etc/ or contains ...status='hidden' so they don't appear in /schools or /dashboard rankings. Also geocode them or delete the lat/lon so the heatmap doesn't cluster at 0,0.bash /home/school/purge_demo.sh via /admin.Run against any clone of school.34.jugaar.ai:
BASE="https://school.34.jugaar.ai"
# 1. Confirm decoy is fiction
for path in /auth/login /auth/register /onboarding; do
curl -sk "$BASE$path" | grep -ci 'decoy\|fake.password'
done
# Expected: 0 matches on every route
# 2. Confirm broken dashboard links
for slug in test-school-d44f6b govt-high-school-model-town-lahore-showcase-7fc73e; do
curl -sk -o /dev/null -w "%{http_code} " "$BASE/schools/view?slug=$slug"
done
# Expected: 404 404
# 3. Confirm security header absence
curl -sk -I "$BASE/" | grep -iE 'hsts|content-security|x-frame|x-content-type'
# Expected: empty
# 4. Confirm audio_path accepts arbitrary value
curl -sk -c jar "$BASE/auth/login" >/dev/null
CSRF=$(curl -sk -b jar -c jar "$BASE/complaints/new" | grep -oE '_csrf"[^>]*value="[^"]+"' | head -1)
curl -sk -b jar -X POST "$BASE/complaints/new" \
--data-urlencode "_csrf=$CSRF" --data-urlencode "school_id=14" \
--data-urlencode "category_id=14" --data-urlencode "title=audio probe" \
--data-urlencode "description=test" --data-urlencode "is_anonymous=1" \
--data-urlencode "audio_path=/etc/passwd" --data-urlencode "audio_duration=99999" \
-o /dev/null -w "%{http_code}\n"
# Expected: 200 (BAD — should reject)