Files
nilesh.pandClaude Opus 4.8 4d3eb18a6c Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation.

- Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool)
- Frontend: Angular 22 SPA (search, checkout, bookings, dashboard)
- Database: schema, seed data and demo bookings migrations
- Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e),
  plus a generated evidence report with PDF export
- Security: whitebox VAPT report under security/

Secrets (.env, tokens, DB credentials) are git-ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 11:38:01 +05:30

365 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TravelHub — Security Assessment Report</title>
<style>
:root{
--ink:#111827;--body:#374151;--muted:#6b7280;--faint:#9ca3af;
--line:#e5e7eb;--rule:#d1d5db;--bg:#f3f4f6;--card:#fff;--accent:#0b5cd6;
--high:#b3243f;--high-bg:#fdeaee;--med:#b45309;--med-bg:#fef3c7;
--low:#0b5cd6;--low-bg:#eaf1fd;--info:#6b7280;--info-bg:#f3f4f6;
--ok:#067a6e;--ok-bg:#e6f4f1;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--body);
font:15px/1.62 'Inter',-apple-system,'Segoe UI',Roboto,sans-serif}
.sheet{max-width:1000px;margin:0 auto;background:var(--card);box-shadow:0 1px 3px rgba(0,0,0,.08)}
h1,h2,h3,h4{color:var(--ink);margin:0;line-height:1.3}
a{color:var(--accent)}
code{background:var(--bg);padding:1px 5px;border-radius:3px;font-size:.86em}
pre{background:#0f172a;color:#e2e8f0;padding:13px 16px;border-radius:8px;overflow-x:auto;
font:12.5px/1.5 ui-monospace,'SFMono-Regular',Menlo,monospace;margin:12px 0}
pre.sql .k{color:#93c5fd}
.cover{padding:44px 56px 34px;border-bottom:3px solid var(--ink)}
.eyebrow{font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:var(--faint);
font-weight:650;margin-bottom:10px}
.cover h1{font-size:2rem;letter-spacing:-.025em;margin-bottom:6px}
.cover .sub{color:var(--muted);font-size:.92rem;margin:0}
.verdict{display:flex;gap:16px;align-items:flex-start;margin-top:26px;padding:18px 22px;
border-radius:10px;background:var(--ok-bg);border:1px solid #b9e0d8}
.verdict .mark{width:40px;height:40px;border-radius:50%;background:var(--ok);color:#fff;
display:grid;place-items:center;font-size:1.2rem;flex-shrink:0}
.verdict .h{font-size:1.08rem;font-weight:650;color:var(--ink)}
.verdict .t{font-size:.9rem;color:var(--muted)}
.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));
border-top:1px solid var(--line);margin-top:26px}
.facts div{padding:13px 22px 13px 0;border-bottom:1px solid var(--line)}
.facts dt{font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);
font-weight:650;margin-bottom:3px}
.facts dd{margin:0;font-size:.9rem;color:var(--ink)}
section{padding:34px 56px}
section+section{border-top:1px solid var(--line)}
.stitle{display:flex;align-items:baseline;gap:12px;margin-bottom:14px;flex-wrap:wrap}
.stitle .n{color:var(--faint);font-variant-numeric:tabular-nums;font-weight:650;font-size:1.05rem}
.stitle h2{font-size:1.3rem;letter-spacing:-.015em}
.lede{color:var(--muted);max-width:76ch}
.sev{display:inline-block;font-size:.66rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
padding:3px 9px;border-radius:4px;white-space:nowrap}
.sev.high{background:var(--high-bg);color:var(--high)}
.sev.med{background:var(--med-bg);color:var(--med)}
.sev.low{background:var(--low-bg);color:var(--low)}
.sev.info{background:var(--info-bg);color:var(--info)}
.sev.ok{background:var(--ok-bg);color:var(--ok)}
table{width:100%;border-collapse:collapse;font-size:.9rem;margin:6px 0}
th{text-align:left;font-size:.7rem;letter-spacing:.07em;text-transform:uppercase;color:var(--faint);
font-weight:650;padding:0 10px 8px 0;border-bottom:2px solid var(--rule)}
td{padding:10px 10px 10px 0;border-bottom:1px solid var(--line);vertical-align:top}
td.c{white-space:nowrap;font-variant-numeric:tabular-nums}
tr td:first-child{color:var(--faint);font-variant-numeric:tabular-nums;width:34px}
.finding{border:1px solid var(--line);border-left-width:4px;border-radius:8px;
padding:20px 24px;margin-bottom:18px;background:var(--card)}
.finding.high{border-left-color:var(--high)}
.finding.med{border-left-color:var(--med)}
.finding.low{border-left-color:var(--low)}
.finding.info{border-left-color:var(--info)}
.finding h3{font-size:1.06rem;margin-bottom:4px;display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
.finding .id{color:var(--faint);font-size:.8rem;font-weight:650;font-variant-numeric:tabular-nums}
.cwe{font-size:.78rem;color:var(--muted);margin-bottom:14px}
.finding h4{font-size:.8rem;letter-spacing:.05em;text-transform:uppercase;color:var(--faint);
margin:16px 0 6px}
.finding p{margin:0 0 8px}
.rec{background:var(--ok-bg);border:1px solid #b9e0d8;border-radius:7px;padding:12px 15px;margin-top:14px}
.rec h4{color:var(--ok);margin-top:0}
.robust td:first-child{color:var(--faint)}
.pill{font-size:.7rem;font-weight:700;color:var(--ok);background:var(--ok-bg);
padding:2px 8px;border-radius:99px}
footer{padding:24px 56px 40px;color:var(--faint);font-size:.82rem;border-top:1px solid var(--line)}
@media (max-width:720px){.cover{padding:26px 20px}section{padding:24px 20px}footer{padding:20px}}
@media print{
body{background:#fff}.sheet{box-shadow:none;max-width:none}
.cover{padding:0 0 20px}section{padding:22px 0}footer{padding:14px 0 0}
.finding{break-inside:avoid}section{break-inside:avoid-page}
h2,h3{break-after:avoid}pre{break-inside:avoid;white-space:pre-wrap}
a{color:var(--ink);text-decoration:none}
}
</style>
</head>
<body>
<div class="sheet">
<div class="cover">
<div class="eyebrow">Security assessment · Whitebox VAPT</div>
<h1>TravelHub</h1>
<p class="sub">Vulnerability Assessment &amp; Penetration Test &middot; Authorised whitebox engagement<br />
Multi-tenant flight booking — Go API, Angular SPA, PostgreSQL &middot; 12 September 2026</p>
<div class="verdict">
<span class="mark">&#10003;</span>
<span>
<span class="h">Core access controls held; findings are operational and defence-in-depth</span><br />
<span class="t">Tenant isolation, SQL parameterisation, mass-assignment and CORS defences all resisted direct attack.
2 High, 5 Medium, 3 Low and 1 Informational finding were identified, chiefly around
credential handling, least privilege and transport hardening.</span>
</span>
</div>
<dl class="facts">
<div><dt>Engagement</dt><dd>Whitebox (source-assisted)</dd></div>
<div><dt>Targets</dt><dd>API :8080 · SPA :4200 · PostgreSQL 17</dd></div>
<div><dt>Findings</dt><dd>2 High · 5 Medium · 3 Low · 1 Info</dd></div>
<div><dt>Critical / RCE (pre-auth)</dt><dd>None</dd></div>
<div><dt>Method</dt><dd>Static review + dynamic testing</dd></div>
<div><dt>Data handling</dt><dd>Dev DB only; demo data restored</dd></div>
</dl>
</div>
<!-- 1. Executive summary -->
<section>
<div class="stitle"><span class="n">1</span><h2>Executive summary</h2></div>
<p class="lede">
TravelHub is architecturally sound on the issues that most often sink a multi-tenant
application. Tenant isolation held under direct testing, SQL is parameterised throughout, the
JSON decoder rejects unknown fields, request bodies are size-capped, and the Angular client
escapes interpolated data by default. The weaknesses found are <strong>operational and
defence-in-depth</strong>, not broken access control.
</p>
<p class="lede" style="margin-top:12px">
The two most important are a <strong>database superuser connection</strong>, which turns any
application compromise into total database control and nullifies the application-layer tenant
isolation, and a <strong>timing side-channel that backfired</strong>: the mitigation intended to
hide which usernames exist actually makes them <em>more</em> distinguishable. None of the
findings is a remotely exploitable pre-authentication breach on the current localhost
deployment; several become serious the moment the app is exposed beyond localhost.
</p>
<table>
<thead><tr><th>#</th><th>Severity</th><th>Finding</th></tr></thead>
<tbody>
<tr><td>1</td><td><span class="sev high">High</span></td><td>Application connects to PostgreSQL as a superuser (no least privilege)</td></tr>
<tr><td>2</td><td><span class="sev high">High</span></td><td>Username enumeration via a timing oracle — equalisation mitigation is miscalibrated and inverts</td></tr>
<tr><td>3</td><td><span class="sev med">Medium</span></td><td>Bcrypt cost factor 6, below the OWASP-recommended minimum of 10</td></tr>
<tr><td>4</td><td><span class="sev med">Medium</span></td><td>No rate limiting or account lockout on authentication</td></tr>
<tr><td>5</td><td><span class="sev med">Medium</span></td><td>Tenant enumeration via timing (unknown tenant short-circuits before password check)</td></tr>
<tr><td>6</td><td><span class="sev med">Medium</span></td><td>No HTTP security headers (HSTS, nosniff, frame-options, CSP…)</td></tr>
<tr><td>7</td><td><span class="sev med">Medium</span></td><td>Database connections run in plaintext (<code>sslmode=disable</code>)</td></tr>
<tr><td>8</td><td><span class="sev low">Low</span></td><td>Real database password committed in <code>config_test.go</code></td></tr>
<tr><td>9</td><td><span class="sev low">Low</span></td><td>Credentials held reversibly in localStorage and replayed each request</td></tr>
<tr><td>10</td><td><span class="sev low">Low</span></td><td>API stores markup verbatim — no server-side output encoding (defence-in-depth)</td></tr>
<tr><td>11</td><td><span class="sev info">Info</span></td><td><code>RequireAdmin</code> middleware wired to no route; role model unenforced</td></tr>
</tbody>
</table>
</section>
<!-- 2. Robust controls -->
<section>
<div class="stitle"><span class="n">2</span><h2>Controls that resisted attack</h2></div>
<p class="lede">These were actively attacked and held. Recorded so the report is a fair account, not only a list of faults.</p>
<table class="robust">
<thead><tr><th>Control</th><th>Test performed</th><th>Result</th></tr></thead>
<tbody>
<tr><td>SQL injection</td><td><code>' OR '1'='1</code>, <code>'; DROP TABLE bookings;--</code>, UNION in search params</td><td><span class="pill">Parameterised</span></td></tr>
<tr><td>ORDER BY injection</td><td><code>sort=(SELECT 1)</code>, <code>sort=price;DROP</code></td><td><span class="pill">Whitelisted</span></td></tr>
<tr><td>Mass assignment</td><td>POST booking with status, totalAmount, bookingReference, tenantId</td><td><span class="pill">Rejected 400</span></td></tr>
<tr><td>Cross-tenant read (IDOR)</td><td>Fetch another agency's booking / flight by id</td><td><span class="pill">404 not-found</span></td></tr>
<tr><td>Cross-tenant credentials</td><td>Skyline password against Globetrotter tenant</td><td><span class="pill">401</span></td></tr>
<tr><td>Large-body DoS</td><td>3&nbsp;MB request body</td><td><span class="pill">400 — 1MB cap</span></td></tr>
<tr><td>CORS</td><td>Reflect <code>Origin: evil.example.com</code></td><td><span class="pill">Not reflected</span></td></tr>
<tr><td>Error verbosity</td><td>Malformed UUID / JSON</td><td><span class="pill">Generic</span></td></tr>
<tr><td>Reflected XSS</td><td>Angular template sinks</td><td><span class="pill">Auto-escaped</span></td></tr>
</tbody>
</table>
</section>
<!-- 3. Detailed findings -->
<section>
<div class="stitle"><span class="n">3</span><h2>Detailed findings</h2></div>
<div class="finding high">
<h3><span class="id">FINDING-1</span> Application connects to PostgreSQL as a superuser <span class="sev high">High</span></h3>
<div class="cwe">CWE-250 Execution with Unnecessary Privileges · OWASP A04:2021 Insecure Design</div>
<h4>Evidence</h4>
<p>The application connects with the <code>backend/.env</code> credentials (<code>DB_USER=postgres</code>), which is the cluster superuser:</p>
<pre>rolname | rolsuper | rolcreatedb | rolcreaterole | rolbypassrls
postgres | t | t | t | t</pre>
<h4>Impact</h4>
<p>Tenant isolation is enforced entirely in application code (every query filters <code>tenant_id</code>), so the DB connection is the last line of defence — and it has none. Any query-control flaw becomes full read/write/DDL over <em>every</em> tenant, plus <code>COPY … TO PROGRAM</code> command execution and <code>rolbypassrls</code> defeating any future row-level security. The superuser connection silently converts application bugs into total compromise.</p>
<div class="rec"><h4>Recommendation</h4>
<p style="margin-bottom:6px">Connect as a least-privilege role; keep migrations under a separate higher-privilege role the service never uses.</p>
<pre>CREATE ROLE travelapp LOGIN PASSWORD '…';
GRANT CONNECT ON DATABASE travelhub TO travelapp;
GRANT USAGE ON SCHEMA public TO travelapp;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO travelapp;
-- NOT: SUPERUSER, CREATEROLE, CREATEDB, TRUNCATE, DDL</pre>
</div>
</div>
<div class="finding high">
<h3><span class="id">FINDING-2</span> Username enumeration via an inverted timing oracle <span class="sev high">High</span></h3>
<div class="cwe">CWE-208 Observable Timing Discrepancy · CWE-203 Observable Discrepancy</div>
<h4>Evidence</h4>
<p>The auth middleware runs a dummy bcrypt comparison for unknown users to equalise timing — a correct instinct — but the dummy hash is <strong>cost 10</strong> while the real stored hashes are <strong>cost 6</strong>. Verifying a cost-10 hash costs ~16× a cost-6 hash, so the "user does not exist" path is far <em>slower</em>. Measured (mean of 30):</p>
<pre>valid user + wrong password : 5.1 ms
unknown user, valid tenant : 47.3 ms &larr; ~9x slower</pre>
<h4>Impact</h4>
<p>An unauthenticated attacker distinguishes valid from invalid usernames with a single request each, at a ~9× margin needing no statistics — enumerating the tenant's user directory for phishing or focused brute force. The mitigation did not merely fail; it created a <em>stronger</em> oracle than doing nothing, because the two branches now use different work factors.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Make the dummy hash's cost identical to the real one and pin both. Better: verify against a per-deployment constant hash generated at startup with the exact configured cost, so the two branches can never drift apart.</p></div>
</div>
<div class="finding med">
<h3><span class="id">FINDING-3</span> Bcrypt cost factor below recommended minimum <span class="sev med">Medium</span></h3>
<div class="cwe">CWE-916 Password Hash With Insufficient Computational Effort</div>
<h4>Evidence</h4>
<p>All stored hashes carry the <code>$2a$06$</code> prefix (cost 6); <code>002_seed.sql</code> relies on pgcrypto <code>gen_salt('bf')</code>, whose default is 6. The test helper uses <code>bcrypt.MinCost</code> (4), so production hashing strength is never exercised by the suite.</p>
<h4>Impact</h4>
<p>Cost 6 is 16× weaker than the OWASP cost-10 floor and 64× weaker than cost 12. Against an offline attacker who obtains the hashes (see Finding&nbsp;1) it materially shortens crack time.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Hash at <strong>cost 12 in Go application code</strong> (<code>bcrypt.GenerateFromPassword(pw, 12)</code>) rather than in SQL, so the work factor lives with the app and stays consistent. Support rehash-on-login for old hashes.</p></div>
</div>
<div class="finding med">
<h3><span class="id">FINDING-4</span> No rate limiting or account lockout on authentication <span class="sev med">Medium</span></h3>
<div class="cwe">CWE-307 Improper Restriction of Excessive Authentication Attempts · A07:2021</div>
<h4>Evidence</h4>
<pre>25 failed logins in 144 ms &rarr; 25x HTTP 401 (no 429, no delay, no lockout)</pre>
<h4>Impact</h4>
<p>Online brute force and credential stuffing are unthrottled. Combined with Finding&nbsp;2 (valid usernames handed to the attacker) and Finding&nbsp;3 (weaker hashes), account takeover is realistic once exposed beyond localhost.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Add per-IP and per-account throttling with exponential backoff and temporary lockout, as a middleware in the existing <code>Chain</code> (token-bucket keyed on IP + <code>X-Tenant</code> + email). Return <code>429</code> with <code>Retry-After</code>; log repeated failures for alerting.</p></div>
</div>
<div class="finding med">
<h3><span class="id">FINDING-5</span> Tenant enumeration via timing <span class="sev med">Medium</span></h3>
<div class="cwe">CWE-204 Observable Response Discrepancy</div>
<h4>Evidence</h4>
<p>An unknown tenant short-circuits before any password check, so it is far faster than a known tenant with bad credentials:</p>
<pre>unknown tenant : 0.8 ms
valid tenant, wrong pw : 5.1 ms</pre>
<h4>Impact</h4>
<p>Reveals which agency slugs exist — including <em>suspended</em> tenants, which the public <code>GET /api/tenants</code> endpoint deliberately hides. Lower impact than Finding&nbsp;2 since active tenants are already public by design.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Perform the dummy bcrypt comparison on the unknown-tenant path too, so all failure modes cost the same. Fold into the Finding&nbsp;2 fix.</p></div>
</div>
<div class="finding med">
<h3><span class="id">FINDING-6</span> Missing HTTP security headers <span class="sev med">Medium</span></h3>
<div class="cwe">CWE-693 Protection Mechanism Failure · A05:2021 Security Misconfiguration</div>
<h4>Evidence</h4>
<p>API responses carry only <code>Content-Type</code>, <code>Content-Length</code>, <code>Date</code>. Absent: <code>Strict-Transport-Security</code>, <code>X-Content-Type-Options</code>, <code>X-Frame-Options</code>, <code>Content-Security-Policy</code>, <code>Referrer-Policy</code>, and <code>Cache-Control</code> on authenticated JSON. The SPA ships no CSP.</p>
<h4>Impact</h4>
<p>No MIME-sniffing protection, no clickjacking defence, no CSP to blunt a future XSS, and authenticated responses may be cached. Individually minor; collectively they remove several cheap layers.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Add a <code>SecureHeaders</code> middleware to the <code>Chain</code>: <code>nosniff</code>, <code>X-Frame-Options: DENY</code>, <code>Referrer-Policy: no-referrer</code>, <code>Cache-Control: no-store</code> on authenticated responses, and HSTS once TLS terminates. Serve the SPA with a restrictive CSP.</p></div>
</div>
<div class="finding med">
<h3><span class="id">FINDING-7</span> Database connections run in plaintext <span class="sev med">Medium</span></h3>
<div class="cwe">CWE-319 Cleartext Transmission of Sensitive Information</div>
<h4>Evidence</h4>
<p><code>.env</code> sets <code>DB_SSLMODE=disable</code>; the live connection reports <code>PLAINTEXT</code> via <code>pg_stat_ssl</code>.</p>
<h4>Impact</h4>
<p>Negligible on localhost. Once the database is on a separate host, credentials and PII (passenger names, passport numbers, contact details) cross the network unencrypted.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Use <code>sslmode=verify-full</code> with a pinned CA for any non-local database; keep <code>disable</code> only for loopback dev, and document that production must differ.</p></div>
</div>
<div class="finding low">
<h3><span class="id">FINDING-8</span> Real database password committed in test source <span class="sev low">Low</span></h3>
<div class="cwe">CWE-798 Use of Hard-coded Credentials</div>
<h4>Evidence</h4>
<p><code>config_test.go</code> contains the actual development DB password as a test literal for the DSN-escaping test.</p>
<h4>Impact</h4>
<p>Low today (dev password, not yet a git repo), but it will enter git history on first push and persist forever, especially damaging if the password is reused. Secrets in fixtures are a common leak vector.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Use a synthetic password that still exercises escaping (e.g. <code>p@ss:w/rd?#%</code>). Add a secret-scanning pre-commit hook (gitleaks) before the first commit.</p></div>
</div>
<div class="finding low">
<h3><span class="id">FINDING-9</span> Credentials stored reversibly client-side and replayed <span class="sev low">Low</span></h3>
<div class="cwe">CWE-522 Insufficiently Protected Credentials · CWE-312 Cleartext Storage</div>
<h4>Evidence</h4>
<p><code>auth.service.ts</code> stores <code>btoa(email:password)</code> in <code>localStorage</code> and replays it on every request. base64 is encoding, not encryption — the plaintext password is trivially recoverable.</p>
<h4>Impact</h4>
<p>Inherent to HTTP Basic auth (the README flags it). Concrete risk: any XSS or compromised SPA dependency reads the reusable password from <code>localStorage</code> — worse than a short-lived token — and it travels on every request.</p>
<div class="rec"><h4>Recommendation</h4>
<p>As the README foresees, move to short-lived tokens with refresh in an <code>HttpOnly; Secure; SameSite</code> cookie. Auth logic is already isolated to two backend packages and the Angular service, so the change is contained.</p></div>
</div>
<div class="finding low">
<h3><span class="id">FINDING-10</span> No server-side output encoding of stored data <span class="sev low">Low</span></h3>
<div class="cwe">CWE-79 Stored XSS sink (currently unreachable)</div>
<h4>Evidence</h4>
<p>A passenger created with <code>firstName = &lt;script&gt;alert(document.cookie)&lt;/script&gt;</code> is stored and returned verbatim by the API.</p>
<h4>Impact</h4>
<p><strong>Not currently exploitable</strong> — the only consumer is Angular, which output-encodes, and no <code>innerHTML</code> sink exists. Latent: a future consumer rendering this as HTML (email, PDF ticket, admin tool, export) would execute it.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Treat the API as the trust boundary: normalise/validate free-text on input and ensure any non-Angular renderer encodes for its context. Do not rely solely on client escaping.</p></div>
</div>
<div class="finding info">
<h3><span class="id">FINDING-11</span> Role model implemented but unenforced <span class="sev info">Info</span></h3>
<div class="cwe">CWE-1220 Insufficient Granularity of Access Control (latent)</div>
<h4>Evidence</h4>
<p><code>RequireAdmin</code> exists and is unit-tested, and users carry <code>admin</code>/<code>agent</code> roles, but <code>router.go</code> gates no route with it — every authenticated user reaches every endpoint.</p>
<h4>Impact</h4>
<p>None today (no admin-only functionality). Flagged because the moment one is added it must be remembered to attach <code>RequireAdmin</code>, or it will be silently open to all agents — a broken-access-control regression waiting to happen.</p>
<div class="rec"><h4>Recommendation</h4>
<p>Gate admin functionality explicitly when introduced and add a test asserting an <code>agent</code> receives 403, mirroring the existing <code>TestRequireAdmin</code>. Prefer a default-deny posture for new routes.</p></div>
</div>
</section>
<!-- 4. Remediation -->
<section>
<div class="stitle"><span class="n">4</span><h2>Remediation priority</h2></div>
<table>
<thead><tr><th>Priority</th><th>Findings</th><th>Rationale</th></tr></thead>
<tbody>
<tr><td class="c"><strong>Before any non-local deployment</strong></td><td class="c">1, 2, 4, 6, 7</td><td>These turn from low-risk to serious the instant the app leaves localhost</td></tr>
<tr><td class="c"><strong>Soon</strong></td><td class="c">3, 5, 8</td><td>Cheap fixes that raise the cost of the attack paths above</td></tr>
<tr><td class="c"><strong>As the design evolves</strong></td><td class="c">9, 10, 11</td><td>Tie to the planned auth change and to new features</td></tr>
</tbody>
</table>
<p class="lede" style="margin-top:14px">
A single focused change set closes most high/medium items: a least-privilege DB role (1);
<code>SecureHeaders</code> + rate-limiting middleware on the existing <code>Chain</code> (4, 6);
aligned bcrypt cost with hashing moved into Go (2, 3, 5); and <code>sslmode</code> flipped for
non-local (7).
</p>
</section>
<!-- 5. Method & limitations -->
<section>
<div class="stitle"><span class="n">5</span><h2>Methodology &amp; limitations</h2></div>
<ul class="lede" style="padding-left:20px">
<li>Testing was against a single-node localhost deployment with seeded data. A production
topology behind a reverse proxy / WAF / TLS terminator may already mitigate some
network-layer findings (6, 7) — verify against the real deployment.</li>
<li>Timing was measured over loopback (mean of 30 requests); absolute values differ over a
network, but the <em>relative</em> discrepancies behind Findings 2 and 5 are structural
(different work factors / short-circuit paths) and persist.</li>
<li>No load, fuzzing, or dependency-CVE (SCA) scanning was performed. Recommend adding
<code>govulncheck</code> and <code>npm audit</code> / OWASP Dependency-Check to CI as a follow-up.</li>
<li>Scope was the application. Host hardening, PostgreSQL <code>pg_hba.conf</code> / listen
configuration, and OS patching were out of scope.</li>
</ul>
</section>
<footer>
Authorised whitebox VAPT of the TravelHub codebase and its localhost instances. Findings are the
assessor's own; verify each against your target environment before and after remediation.
Print this page (or use <code>npm run report</code>-style export) to produce a PDF.
</footer>
</div>
</body>
</html>