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.
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.
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)
The template is escaping already-escaped HTML. On /notices a real visitor reads:
<p>All IT companies operating in Sindh must renew their PSHE (Provincial Shop & Establishment) licences…</p><p>The 2% Software Export Incentive for FY2026-27 is now open…</p>On /programs/1 the visitor reads:
Incubation <p>A 6-month incubation program for IT startups…</p><ul> <li>Registered IT company (PSEB or PSHE)\n- Fewer than 50 employees…</li> </ul> — rendered as one giant blob, no list bullets, no line breaksSource confirmation via curl -s /notices | grep '&' shows Shop & Establishment in the body — the canonical sign of double-escape (Blade's {{ }} auto-escape + an upstream `htmlspecialchars()` somewhere in the pipeline).
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.
Find the second escape pass. Likely culprits:
htmlspecialchars() on body fields before they hit the templatehtmlspecialchars() on save AND Blade auto-escaping on renderThe 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.
Where: POST /logout (triggered by "Logout" button in dashboard top nav)
cloud9@demo.com / demo12345HTTP/1.1 500 Internal Server Error, body is the Laravel error page (full stack trace leak — see SITFD-016)curl -s -c c.txt https://sitfd2.production1.jugaar.ai/login > /dev/null
CSRF=$(grep -oE 'name="_token" value="[^"]*"' c.txt | head -1 | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//')
curl -s -b c.txt -X POST https://sitfd2.production1.jugaar.ai/login \
-d "_token=$CSRF&email=cloud9@demo.com&password=demo12345" -o /dev/null
curl -s -b c.txt -X POST https://sitfd2.production1.jugaar.ai/logout -i | head -1
# Expected: 302 Found → /
# Actual: 500 Internal Server Error
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.
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).
Where: https://sitfd2.production1.jugaar.ai/login — "Demo quick logins (testing)" section
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.
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.
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
Where: Every page when "اردو" link is clicked
/programs/1, clicked "اردو" → landed on / (homepage, not back to /programs/1)/lang/ur returns 302 → / (verified in headers — `Location: https://sitfd2.production1.jugaar.ai`)<html lang=""> attribute is not set to "ur" in Urdu mode — screen readers will not switch to Urdu pronunciation. (The English homepage uses <html lang="en" dir="ltr">.)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.
Three fixes needed together:
<a href="{{ url('/lang/ur') }}?return={{ url()->current() }}">اردو</a>, and have the controller redirect to $request->query('return', '/')resources/lang/ur/ translation files for every string in the body (or use a CMS that supports localized content per page). At minimum translate headings + nav + footer + button labels.<html lang="{{ app()->getLocale() }}" dir="{{ in_array(app()->getLocale(), ['ur', 'ar']) ? 'rtl' : 'ltr' }}"> so RTL CSS kicks in and screen readers switch languageWhere: Primary nav on every page (visible to every visitor on every page load)
View-source of any page contains:
<a href="https://sitfd2.production1.jugaar.ai/kb">Help & FAQ</a>
Browser renders this as the literal text "Help & FAQ" instead of "Help & FAQ". Confirmed across all 17 tested pages.
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".
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.
Where: <title> tag on /service-standard/about, /service-standard/service-standard, /service-standard/privacy
/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.
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.
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.
Where: Top-left logo on every page (header brand block)
curl -I /img/logo.svg → 404 Not Found, Content-Length: 162bA government portal shipping with a broken logo image. The 162-byte 404 response is also a small wasted request on every page load.
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.
Where: Homepage stats row (5th KPI card) and identical stat on /public-index
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.
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.
sla_compliance_pct is null. Don't ship empty KPIs./kb/sla already.Where: POST /track with bad reference
POST with ref=BAD-9999-12345 → 302 → /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.
"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.
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.
Where: /sitemap.xml, /sitemap_index.xml, /sitemap.xml.gz
All sitemap paths return 404. /robots.txt does exist and is permissive (User-agent: * Disallow:) but points at no sitemap.
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.
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.
Where: <head> of every page
Homepage <head> contains only: charset, viewport, description, title, csrf-token. No og:title, og:description, og:image, og:url, twitter:card, or twitter:image.
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.
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">
Where: Homepage hero, 5-stat row
"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.
First-impression polish issue on the most-visited page of the portal.
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.
Where: /notices/:id and /programs/:id (deep-link URLs)
Confirmed: curl -I /notices/1 → 404 Not Found. Only the listing pages exist. Each notice currently has no permalink.
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.
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.
Where: Browser tab on every page
curl -I /favicon.ico → HTTP/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.
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.
Where: Demo accounts (see SITFD-003 for the broader credential-leak issue)
All 8 demo accounts share password demo12345. Combined with SITFD-003, this means view-source yields a single common password for all 8 accounts.
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.
Where: Error responses on production
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.
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.
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.
Where: /.well-known/security.txt (RFC 9116)
Returns 403 (the dir exists, the file doesn't). No security contact path for vulnerability reporters.
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
| Feature / Capability | Status |
|---|---|
| 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 header | ✅ Strict-Transport-Security: max-age=31536000; includeSubDomains |
| Content Security Policy | ✅ Strict CSP with no unsafe-inline on scripts |
| X-Frame-Options / clickjacking protection | ✅ SAMEORIGIN |
| X-Content-Type-Options / MIME sniffing protection | ✅ nosniff |
| Referrer-Policy | ✅ strict-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 | Status | TTFB | Title |
|---|---|---|---|
| GET / | 200 OK | 0.41s | Home · SITFD |
| GET /kb | 200 OK | 0.31s | Help & FAQ · SITFD |
| GET /programs | 200 OK | 0.31s | Programs & Schemes · SITFD |
| GET /programs/1 | 200 OK | 0.32s | Digital Sindh — Startup Incubation · SITFD |
| GET /programs/2 | 200 OK | 0.30s | Software Export Incentive 2026 · SITFD |
| GET /programs/3 | 200 OK | 0.31s | IT Sector Capacity Building 2026 · SITFD |
| GET /notices | 200 OK | 0.31s | Official Notices · SITFD |
| GET /notices/1 | 404 | — | — |
| GET /public-index | 200 OK | 0.35s | Redressal Index · SITFD |
| GET /login | 200 OK | 0.29s | Login · SITFD |
| POST /login (valid creds) | 302 → /dashboard | — | — |
| POST /logout | 500 | — | — |
| GET /register | 200 OK | 0.30s | Register · SITFD |
| GET /lang/ur | 302 → / | — | (no Urdu content on landing) |
| GET /lang/en | 302 → / | — | — |
| GET /track | 200 OK | 0.30s | Track a ticket · SITFD |
| POST /track (invalid ref) | 302 → /track (silent) | — | — |
| GET /service-standard/about | 200 OK | 0.32s | About / Service Standard · SITFD (DUPLICATE) |
| GET /service-standard/service-standard | 200 OK | 0.31s | About / Service Standard · SITFD (DUPLICATE) |
| GET /service-standard/privacy | 200 OK | 0.30s | About / Service Standard · SITFD (DUPLICATE) |
| GET /open-data | 200 (CSV attachment) | 0.34s | — |
| GET /robots.txt | 200 OK | — | — |
| GET /sitemap.xml | 404 | — | — |
| GET /favicon.ico | 200 (0 bytes!) | — | — |
| GET /img/logo.svg | 404 | — | — |
| GET /css/app.css | 200 OK (23 KB) | — | — |
| GET /js/app.js | 200 OK (3.5 KB) | — | — |
| GET /vendor/chart.min.js | 200 OK (205 KB) | — | — |
| GET /admin (no auth) | 302 → /login | — | — |
| GET /admin (logged in, non-admin) | 403 | — | — |
| GET /api | 404 | — | — |
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.
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 '&' # should be 0