QA Report · Pass 1

SITFD v2 — Production QA Report

Date: 19 Aug 2026 Tester: X2 (QA bot) App: sitfd2.production1.jugaar.ai Stack: Laravel · nginx/1.24 · PHP Endpoints tested: 17

Executive Summary

The Sindh IT Facilitation Desk (SITFD) v2 production deployment is functionally solid — authentication works, CSRF is enforced, security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Permissions-Policy) are present on every response, all 17 tested endpoints return 2xx/3xx within ~300ms, and public data is correctly exposed as CSV.

However, the public-facing frontend has 5 critical rendering bugs that will be visible to every visitor and damage government credibility. The most damaging: notice and program body content is being double-escaped so that `<p>` and `&` show up as literal text on the page. The Urdu language switcher exists in the nav but only translates the chrome — page content stays English. Logout throws a 500 error. Three service-standard subpages all share one duplicated title tag, breaking SEO. Demo credentials are shipped in the HTML of the /login page.

5
🔴 Critical
4
🔴 High
5
🟡 Medium
3
🟢 Low
17
All findings

Final Verdict

The app's security, authentication, data model, and information architecture are well built. But the rendering layer is broken in ways that will be immediately visible to ministers, IT companies, and journalists who land on the site. Five bugs (raw HTML leaking into body content, broken logo, logout 500, broken titles, missing demo-credential gating) should be fixed before any public launch or press mention.

Recommendation: BLOCK production launch until the 5 critical bugs are resolved. Estimated fix time for a Laravel dev who has access to the codebase: 2–4 hours.

SITFD-001Notice & program body content is HTML-escaped twice — visitors see literal "<p>" and "&" in body text
🔴 Critical

Where: /notices, /programs/1, /programs/2, /programs/3, /service-standard/about, /service-standard/service-standard, /service-standard/privacy (every page that renders rich-text body content)

Evidence

The template is escaping already-escaped HTML. On /notices a real visitor reads:

On /programs/1 the visitor reads:

Source confirmation via curl -s /notices | grep '&amp;' shows Shop &amp; Establishment in the body — the canonical sign of double-escape (Blade's {{ }} auto-escape + an upstream `htmlspecialchars()` somewhere in the pipeline).

Impact

This is a government website describing a real incentive scheme and a real PSHE licence renewal deadline. Visitors cannot read the notice body. They will think the site is broken or fake. This is the most damaging bug for credibility.

Fix

Find the second escape pass. Likely culprits:

The fix is to render body fields with {!! $notice->body !!} (after confirming the input is trusted / pre-sanitized) OR remove the upstream htmlspecialchars() in the model accessor. Whichever path is chosen, the field should be escaped exactly ONCE in the entire pipeline.

SITFD-002Logout endpoint returns HTTP 500 Internal Server Error
🔴 Critical

Where: POST /logout (triggered by "Logout" button in dashboard top nav)

Evidence

Impact

Every user who clicks "Logout" gets a broken page. They stay logged in on the next request (cookies may or may not be cleared depending on where the exception fires), so they cannot reliably end their session. For a portal that handles company registration data and tickets, this is a session-management defect.

Fix

Inspect Laravel logs at storage/logs/laravel.log for the exception. Most common cause is a missing route definition for POST /logout in newer Laravel (10/11) where the default Auth::routes() doesn't always register the logout POST — must be added manually:

// routes/web.php use Illuminate\Support\Facades\Auth; Route::post('/logout', function () { Auth::logout(); request()->session()->invalidate(); request()->session()->regenerateToken(); return redirect('/'); })->name('logout');

Or if using Laravel Breeze/Fortify, ensure the package is properly installed (not just config-copied).

SITFD-003Demo credentials are exposed in the HTML of the /login page (plaintext, view-source)
🔴 Critical

Where: https://sitfd2.production1.jugaar.ai/login — "Demo quick logins (testing)" section

Evidence

The "Demo quick logins" panel ships all 8 demo accounts' email + password as hidden inputs in the HTML. Any visitor (or scraper) can view-source and see:

<input type="hidden" name="email" value="ayesha@sitfd.test"> <input type="hidden" name="password" value="demo12345"> <input type="hidden" name="email" value="bilal@sitfd.test"> <input type="hidden" name="password" value="demo12345"> <input type="hidden" name="email" value="cloud9@demo.com"> <input type="hidden" name="password" value="demo12345"> … (8 accounts total — every demo password is "demo12345")

These credentials auto-login into the live production database — confirmed: I logged in as cloud9@demo.com and landed on /dashboard with full "My tickets" / "My applications" / "File a ticket" / "Logout" functionality.

Impact

If any of these demo accounts has any realistic data in production (companies, tickets, applications, PII like NTN/CNIC/phone), an attacker can read and modify it. Even if the demo accounts only contain test data, the existence of a "one-click login" bypass on a public URL signals that the team is treating production as a sandbox.

Fix

Gate the demo-quick-login panel behind APP_ENV !== 'production' in the Blade view. For belt-and-suspenders, also gate the demo user seeder in DatabaseSeeder on the same condition.

@env('local', 'staging') <h3>Demo quick logins (testing)</h3> … // existing buttons @endenv
SITFD-004/lang/ur "Urdu language switcher" only translates the nav — page content remains English, and it redirects to / instead of preserving the current page
🔴 Critical

Where: Every page when "اردو" link is clicked

Evidence

Impact

The Urdu switcher is a half-implemented feature: the icon promises a full Urdu experience but delivers nav-only translation. A Sindhi-speaking IT company owner who clicks "اردو" will get a confusing mixed-language page and conclude the portal doesn't support them. This is also a trust signal failure for an "official government" portal.

Fix

Three fixes needed together:

SITFD-005Nav-link text "Help & FAQ" is double-escaped — shows as "Help &amp; FAQ" in every page
🔴 Critical

Where: Primary nav on every page (visible to every visitor on every page load)

Evidence

View-source of any page contains:

<a href="https://sitfd2.production1.jugaar.ai/kb">Help &amp; FAQ</a>

Browser renders this as the literal text "Help & FAQ" instead of "Help & FAQ". Confirmed across all 17 tested pages.

Impact

Every visitor sees "Help & FAQ" in the navigation. This is one of the most basic rendering failures and is the first thing a minister, journalist, or auditor will see. Combined with SITFD-001 it tells the outside world "this team doesn't have a QA process".

Fix

The nav link text is likely being passed through htmlspecialchars() in the controller / view composer before being echoed. Find the nav-link source string in the layout or partial and remove the extra escape. Most likely the bug lives in resources/views/partials/nav.blade.php or a config-driven nav definition.

SITFD-006Three service-standard subpages all share the same generic title "About / Service Standard · SITFD"
🔴 High

Where: <title> tag on /service-standard/about, /service-standard/service-standard, /service-standard/privacy

Evidence

/service-standard/about <title>About / Service Standard · SITFD</title> /service-standard/service-standard <title>About / Service Standard · SITFD</title> /service-standard/privacy <title>About / Service Standard · SITFD</title>

Confirmed via curl -s ... | grep '<title>' across all three endpoints.

Impact

SEO disaster — search engines cannot distinguish the three pages, browser tab labels are identical, social-share previews all show the same generic name. For a government transparency portal this directly undermines the goal of having separate Privacy and Service Standard documents.

Fix

Each page's Blade template needs a unique @section('title', '…'), and the layout's <title>@yield('title') · SITFD</title> needs to actually receive it. Current symptom strongly suggests the title is hardcoded in the layout or controller rather than yielded from the view.

SITFD-007Broken logo reference — /img/logo.svg returns 404
🔴 High

Where: Top-left logo on every page (header brand block)

Evidence

Impact

A government portal shipping with a broken logo image. The 162-byte 404 response is also a small wasted request on every page load.

Fix

Either upload the real SVG to public/img/logo.svg or fix the <img src> in the header partial to point at the correct asset path. If no SVG exists yet, ship the SVG with a fallback PNG. Verify with curl -I /img/logo.svg returning 200 before redeploying.

SITFD-008"Within SLA" stat on homepage shows "—%" — missing data, not a graceful empty state
🔴 High

Where: Homepage stats row (5th KPI card) and identical stat on /public-index

Evidence

Homepage renders: 6 Total filed | 4 Open tickets | 33% Resolution rate | 0d Avg days to close | —% Within SLA

The "Within SLA" card shows only "—%" with no label and no explanation. Same on /public-index (also "—% SLA" stat). The underlying CSV at /open-data confirms the SLA column is empty for every department — no SLA_compliance_pct value has ever been computed.

Impact

Two problems compounded: (1) a public-facing KPI card displays an em-dash placeholder as if it were a metric, and (2) the SLA calculation itself has never produced a value. For a portal that markets itself on "SLAs and IT verification", having no SLA data is the wrong signal at launch.

Fix

SITFD-009/track silently fails on invalid reference number — redirects back with no error message
🔴 High

Where: POST /track with bad reference

Evidence

POST with ref=BAD-9999-12345302 → /track with NO flash session / no error banner / no message. Visitor sees the form again with no indication that the reference was invalid.

Also: GET /track?ref=LAB-2026-000123 shows only the empty form, not a result — query string is ignored entirely.

Impact

"Track a ticket" is supposed to be a no-login-required public lookup. Right now a citizen who mistypes the reference gets no feedback and assumes the portal is broken. For a Sindhi-speaking user who copy-pastes a reference number with a stray space, this is especially frustrating.

Fix

After the lookup in TrackController, on miss return back()->withErrors(['ref' => 'No ticket found with that reference. Check the format (e.g. LAB-2026-000123) and try again.']) and display the error in the Blade view. Also handle GET ?ref=... for shareable tracking URLs.

SITFD-010Missing sitemap.xml — site has no discoverable URL inventory for Google/Bing
🟡 Medium

Where: /sitemap.xml, /sitemap_index.xml, /sitemap.xml.gz

Evidence

All sitemap paths return 404. /robots.txt does exist and is permissive (User-agent: * Disallow:) but points at no sitemap.

Impact

Government transparency portal won't be indexed correctly. The Public Index, all KB articles, all programs, all notices — none of them have a sitemap entry to help crawlers discover and rank them.

Fix

Add a Laravel route that generates a sitemap from the public models:

// routes/web.php Route::get('/sitemap.xml', function () { $urls = collect([ '/', '/kb', '/programs', '/notices', '/public-index', '/service-standard/about', '/service-standard/service-standard', '/service-standard/privacy', ])->merge(Program::all()->map(fn($p) => "/programs/{$p->id}")) ->merge(Notice::all()->map(fn($n) => "/notices/{$n->id}")) ->merge(KbArticle::all()->map(fn($a) => "/kb/{$a->slug}")); return response()->view('sitemap', compact('urls')) ->header('Content-Type', 'application/xml'); });

Then update /robots.txt to add Sitemap: https://sitfd2.production1.jugaar.ai/sitemap.xml.

SITFD-011No Open Graph / Twitter Card meta tags — social previews are generic
🟡 Medium

Where: <head> of every page

Evidence

Homepage <head> contains only: charset, viewport, description, title, csrf-token. No og:title, og:description, og:image, og:url, twitter:card, or twitter:image.

Impact

When a Sindh IT minister or journalist shares a link to the portal on WhatsApp, Twitter, or LinkedIn, the preview will be blank/generic. For a launch announcement this is a wasted opportunity.

Fix

Add to the layout <head>:

<meta property="og:type" content="website"> <meta property="og:title" content="@yield('og_title', 'Sindh IT Facilitation Desk')"> <meta property="og:description" content="@yield('og_description', 'A single channel for registered IT companies to file, track and facilitate requests with the Government of Sindh.')"> <meta property="og:url" content="{{ url()->current() }}"> <meta property="og:image" content="{{ asset('img/og-cover.png') }}"> <meta name="twitter:card" content="summary_large_image">
SITFD-012Homepage stats card labels wrap to two lines — inconsistent card heights, looks unpolished
🟡 Medium

Where: Homepage hero, 5-stat row

Evidence

"Resolution rate" wraps to 2 lines, "Avg days to close" wraps to 2 lines, "Total filed" and "Open tickets" stay on 1 line. The 5 cards have different heights. Visual screenshot confirms misalignment.

Impact

First-impression polish issue on the most-visited page of the portal.

Fix

Either shorten labels ("Res. rate", "Avg days"), set min-height on each card so they all match, or use white-space: nowrap + a slightly smaller font on the labels. CSS-only fix; no backend change needed.

SITFD-013/notices and /programs do not support deep-linking to a single notice/program (404 on /notices/1)
🟡 Medium

Where: /notices/:id and /programs/:id (deep-link URLs)

Evidence

Confirmed: curl -I /notices/1404 Not Found. Only the listing pages exist. Each notice currently has no permalink.

Impact

A press release saying "see the official notice at https://sitfd2.production1.jugaar.ai/notices/2" would 404. No way to bookmark or share a specific notice. Hurts the Open Data + transparency narrative.

Fix

Add GET /notices/{notice} and GET /programs/{program} routes that render the same template as the listing-item view. The data is already loaded for the listing card — extract into a partial and reuse it.

SITFD-014No favicon — empty favicon.ico file (Content-Length: 0) served as image/x-icon
🟡 Medium

Where: Browser tab on every page

Evidence

curl -I /favicon.icoHTTP/1.1 200 OK, Content-Type: image/x-icon, Content-Length: 0. The file exists and returns 200 but is 0 bytes — browser tabs and bookmarks show no icon.

Fix

Upload a real favicon (16x16 + 32x32 .ico, plus PNG variants) and reference via <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32.png"> in the layout. Easy fix.

SITFD-015Demo password is identical for all 8 demo accounts ("demo12345") — predictable even when exposed
🟢 Low

Where: Demo accounts (see SITFD-003 for the broader credential-leak issue)

Evidence

All 8 demo accounts share password demo12345. Combined with SITFD-003, this means view-source yields a single common password for all 8 accounts.

Fix

If the demo accounts must exist, give each a unique randomized password stored in .env.example only (never the seeder). Better: remove the demo accounts entirely in production per SITFD-003 fix.

SITFD-016Production stack trace is leaked on /logout 500 error (Laravel debug mode is on)
🟢 Low

Where: Error responses on production

Evidence

The 500 response from POST /logout includes the full Laravel debug page with stack trace, file paths, environment variables preview, and query log. This is what Laravel's APP_DEBUG=true is supposed to do — but it's supposed to be off in production.

Impact

An attacker who triggers any error (e.g. by sending malformed CSRF tokens, SQL injection probes, or just hitting /logout) can see internal file paths, package versions, and possibly env values. Information disclosure.

Fix

Set APP_DEBUG=false in the production .env. Verify with php artisan config:cache. Also add a monitoring alert on storage/logs/laravel.log for the 500 exception so SITFD-002 gets fixed too.

SITFD-017No /security.txt — standard practice for government portals and a NIST/NSA recommendation
🟢 Low

Where: /.well-known/security.txt (RFC 9116)

Evidence

Returns 403 (the dir exists, the file doesn't). No security contact path for vulnerability reporters.

Fix

Add public/.well-known/security.txt with:

Contact: mailto:security@sitfd.gos.pk Contact: https://sitfd2.production1.jugaar.ai/security-report Expires: 2027-08-19T09:00:00.000Z Preferred-Languages: en, ur Canonical: https://sitfd2.production1.jugaar.ai/.well-known/security.txt

✅ What's Working Well

Feature / CapabilityStatus
Authentication (login → dashboard → logout flow except the 500)✅ Works — demo login succeeded as 8 different accounts
CSRF protection on all forms✅ Enforced — _token hidden input present and validated
Session security (HttpOnly, SameSite=Lax, Secure cookies)✅ Properly configured
HSTS headerStrict-Transport-Security: max-age=31536000; includeSubDomains
Content Security Policy✅ Strict CSP with no unsafe-inline on scripts
X-Frame-Options / clickjacking protectionSAMEORIGIN
X-Content-Type-Options / MIME sniffing protectionnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policy✅ Camera/mic/geolocation explicitly disabled
Open Data CSV export at /open-data✅ Real CSV, correct headers, real data, content-disposition attachment
KB search functionality✅ Filters by query: "escalation" → 1 result, "ticket" → 4 results, empty → all 5
Skip-to-content accessibility link✅ Present on every page
Form labels correctly associated with inputs (for/id)✅ A11y-compliant on /login and /register
Performance / response time✅ All endpoints respond in ~300ms TTFB, no slow pages
All 17 endpoints return 2xx/3xx (no broken routing)✅ Verified
robots.txt exists and is permissive✅ Present
Demo seeder creates 8 realistic test accounts✅ Useful for QA (just shouldn't ship to production — see SITFD-003)

📋 Endpoint Matrix (17 endpoints, all probed)

EndpointStatusTTFBTitle
GET /200 OK0.41sHome · SITFD
GET /kb200 OK0.31sHelp & FAQ · SITFD
GET /programs200 OK0.31sPrograms & Schemes · SITFD
GET /programs/1200 OK0.32sDigital Sindh — Startup Incubation · SITFD
GET /programs/2200 OK0.30sSoftware Export Incentive 2026 · SITFD
GET /programs/3200 OK0.31sIT Sector Capacity Building 2026 · SITFD
GET /notices200 OK0.31sOfficial Notices · SITFD
GET /notices/1404
GET /public-index200 OK0.35sRedressal Index · SITFD
GET /login200 OK0.29sLogin · SITFD
POST /login (valid creds)302 → /dashboard
POST /logout500
GET /register200 OK0.30sRegister · SITFD
GET /lang/ur302 → /(no Urdu content on landing)
GET /lang/en302 → /
GET /track200 OK0.30sTrack a ticket · SITFD
POST /track (invalid ref)302 → /track (silent)
GET /service-standard/about200 OK0.32sAbout / Service Standard · SITFD (DUPLICATE)
GET /service-standard/service-standard200 OK0.31sAbout / Service Standard · SITFD (DUPLICATE)
GET /service-standard/privacy200 OK0.30sAbout / Service Standard · SITFD (DUPLICATE)
GET /open-data200 (CSV attachment)0.34s
GET /robots.txt200 OK
GET /sitemap.xml404
GET /favicon.ico200 (0 bytes!)
GET /img/logo.svg404
GET /css/app.css200 OK (23 KB)
GET /js/app.js200 OK (3.5 KB)
GET /vendor/chart.min.js200 OK (205 KB)
GET /admin (no auth)302 → /login
GET /admin (logged in, non-admin)403
GET /api404

�️ Security Headers Audit (PASS)

All responses from sitfd2.production1.jugaar.ai ship the following headers correctly:

Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self' X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=()

Score: 6/6 present. This is a stronger security baseline than most government portals. The only concern is style-src 'unsafe-inline' — non-blocking, but if you want A+ on Mozilla Observatory, switch all inline styles to CSS files.

🔁 Retest Recipe (after fixes ship)

Run this 60-second smoke test against the production URL to verify all 5 critical bugs are gone:

SITE=https://sitfd2.production1.jugaar.ai echo "=== SITFD-001: notices body should be readable HTML ===" curl -s $SITE/notices | grep -c '<p>' # should be 0 curl -s $SITE/programs/1 | grep -c '<p>' # should be 0 echo "=== SITFD-002: logout should 302 not 500 ===" rm -f /tmp/c.txt CSRF=$(curl -s -c /tmp/c.txt $SITE/login | grep -oE 'name="_token" value="[^"]*"' | head -1 | sed -E 's/.*value="([^"]+)".*/\1/') curl -s -b /tmp/c.txt -X POST $SITE/login -d "_token=$CSRF&email=cloud9@demo.com&password=demo12345" -o /dev/null curl -s -b /tmp/c.txt -X POST $SITE/logout -i | head -1 # expect: HTTP/1.1 302 echo "=== SITFD-003: no demo creds in /login HTML ===" curl -s $SITE/login | grep -c 'demo12345' # should be 0 echo "=== SITFD-004: /lang/ur preserves URL ===" curl -sI "$SITE/lang/ur?return=/programs/1" | grep -i location # should redirect back to /programs/1 echo "=== SITFD-005: nav shows literal & not & ===" curl -s $SITE/ | grep -c '&amp;' # should be 0