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>
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
<!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 & Penetration Test · Authorised whitebox engagement<br />
|
||||
Multi-tenant flight booking — Go API, Angular SPA, PostgreSQL · 12 September 2026</p>
|
||||
|
||||
<div class="verdict">
|
||||
<span class="mark">✓</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 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 ← ~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 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 → 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 2 (valid usernames handed to the attacker) and Finding 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 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 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 = <script>alert(document.cookie)</script></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 & 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>
|
||||
Reference in New Issue
Block a user