# GLOBAL CODE REVIEW -- Time Lapse Pty Ltd Website (CMS + Research Tool)
**Date**: 2026-03-27
**Path**: /var/www/html/www/
**Reviewer**: Automated Static Analysis

## Summary
- Bugs: 2 critical, 3 high, 4 medium, 3 low
- Vulnerabilities: 3 critical, 3 high, 2 medium, 2 low
- Verdict: **FAIL** (3 critical vulnerabilities require immediate remediation)

## Coverage
- Files reviewed: 62 (all core logic, API endpoints, auth, config, frontend JS, error logging)
- PHP files: 208 | JS files: 59 | HTML files: 658 (includes ~600 design variant pages)
- API endpoints found: 60 (22 admin CMS + 35 research + 3 public)
- Test files found: 44 (20 CMS, 24 research)

## Findings

| # | Type | Severity | File:Line | Issue | Suggested Fix |
|---|------|----------|-----------|-------|---------------|
| 1 | VULN | CRITICAL | dev/includes/auth.php:219-227 | **Authentication completely bypassed.** `requireAuth()` returns a hardcoded admin user without checking any session or cookie. Every admin API endpoint is unprotected -- anyone can create, update, delete all CMS content, upload files, and manage settings without logging in. | Remove the bypass block (lines 219-227) and restore the commented-out session validation logic that follows. |
| 2 | VULN | CRITICAL | dev/includes/auth.php:233-237 | **CSRF protection completely bypassed.** `requireCsrf()` returns immediately without validating any token. Combined with #1, any attacker can perform state-changing operations (POST/PUT/DELETE) on all endpoints with no authentication or CSRF verification. | Remove the bypass block (lines 234-236) and restore the commented-out CSRF validation call. |
| 3 | VULN | CRITICAL | dev/admin/auto-login.html:3, auto-login2.html:3, auto-login-r2a.html:3, auto-login-r2b.html:3, auto-login-r3b.html:3 | **Admin password hardcoded in 5 publicly accessible HTML files.** The password `TimeLapse2026!` is embedded in plaintext JavaScript in 5 different auto-login HTML files. These files are accessible to anyone who can reach the web server. | Delete all auto-login HTML files immediately. If auto-login is needed for development, use environment variables or a server-side mechanism that never exposes credentials to the browser. |
| 4 | VULN | HIGH | dev/admin/api/auth.php:87-98 | **Auth check endpoint always returns authenticated.** The `GET check` action always returns `authenticated: true` with hardcoded admin user data regardless of whether a valid session cookie exists. This means the admin frontend never redirects to login. | Remove the bypass block and restore session validation via `getCurrentUser()`. |
| 5 | VULN | HIGH | dev/admin/api/csrf.php:16-17 | **CSRF endpoint returns dummy token.** Returns the literal string `'bypass'` instead of generating a real cryptographic token. Even if CSRF validation were re-enabled, the dummy token would need to be cleared. | Remove bypass and restore `generateCsrfToken($user['user_id'])` call. |
| 6 | VULN | HIGH | dev/page-preview.php:17-19 | **Page preview authentication bypassed.** Hardcodes an admin user object instead of requiring authentication. Anyone can preview draft content by passing a `page_id` parameter. | Replace bypass with `$authUser = getCurrentUser(); if (!$authUser) { http_response_code(401); exit(); }` |
| 7 | BUG | CRITICAL | dev/admin/api/media.php:235 | **Media PUT endpoint missing CSRF protection.** The PUT handler for updating media metadata (alt_text) does not call `requireCsrf($user)`, unlike POST and DELETE handlers in the same file. Even when CSRF is restored, media updates will remain unprotected. | Add `requireCsrf($user);` at the beginning of the PUT block (after line 235). |
| 8 | BUG | CRITICAL | dev/includes/auth.php:272-296 | **HTML sanitizer bypassable via double-encoding.** The `sanitizeHtml()` function calls `html_entity_decode()` before filtering, which correctly handles single-encoded payloads. However, it only strips event handlers matching `on\w+` with specific quoting patterns. An attacker could use constructions like `<img src=x onerror\t=alert(1)>` (tab before equals) or malformed attributes to bypass the regex-based filtering. Additionally, the `<img>` and `<a>` tags are allowed, and `src` attribute filtering only blocks `javascript:`, `data:`, and `vbscript:` prefixes but does not enforce HTTPS for `<img src>`. | Replace regex-based HTML sanitization with a proper allowlist DOM parser (e.g., HTML Purifier library), or at minimum use DOMDocument to parse and rebuild HTML with only explicitly allowed attributes. |
| 9 | BUG | HIGH | dev/testing/test-cms-content.php:244 | **SQL injection in test file.** `$db->exec("DELETE FROM contact_submissions WHERE id = $subId")` directly interpolates `$subId` into SQL without parameterization. While this is a test file, it sets a bad precedent and could corrupt the test database if `$subId` is ever manipulated. | Use `$db->prepare('DELETE FROM contact_submissions WHERE id = ?')->execute([$subId]);` |
| 10 | BUG | HIGH | dev/research/classes/Database.php:87-91 | **Table/column names not validated in Database class.** The `insert()`, `update()`, and `delete()` methods accept table and column names as strings and interpolate them directly into SQL without validation. The CMS `db.php` has `dbValidateIdentifier()` but the research Database class does not. If any API passes user-controlled data as a table or column name, this is exploitable. | Add an identifier validation method (allow only `[a-zA-Z_][a-zA-Z0-9_]*`) and call it before interpolating table/column names in `insert()`, `update()`, and `delete()`. |
| 11 | BUG | HIGH | dev/research/auth-bridge.php:8-14 | **Research tool authentication silently disabled when auth module not found.** If `auth/check.php` does not exist, the research tool runs completely unauthenticated with no warning. An attacker who can access `/www/research/` endpoints can read/modify all competitive intelligence data. | Replace the silent fallback with an explicit denial: `else { http_response_code(403); exit('Authentication module not available'); }` |
| 12 | VULN | MEDIUM | dev/includes/smtp-config.php:20-26 | **SMTP credentials stored in plaintext file in data directory.** While the file is gitignored and the directory exists on disk, the credentials file `data/smtp-credentials.txt` has 664 permissions, making it group-readable. The smtp-config.php itself is also gitignored, but the SMTP username `cloud1@livetimelapse.com.au` is hardcoded in the file (line 18). | Set `data/smtp-credentials.txt` to 640 permissions. Consider using environment variables for SMTP credentials. |
| 13 | VULN | MEDIUM | dev/admin/api/screenshots.php:97 | **Chrome executed with `--no-sandbox` flag.** The screenshot capture function runs Chrome headless with `--no-sandbox`, which reduces process isolation. If a malicious page is screenshotted, the Chrome process has fewer security boundaries. | Remove `--no-sandbox` and configure proper Chrome sandboxing, or ensure the Chrome user has a proper namespace/cgroup setup. |
| 14 | BUG | MEDIUM | dev/includes/config.php:27-35 | **BASE_URL constructed from potentially spoofable headers.** `HTTP_HOST` is used to construct `BASE_URL`, which is then used in SEO canonical URLs, sitemap generation, and email body content. An attacker sending a crafted `Host` header could poison canonical URLs or redirect links in emails. | Hardcode the production domain or validate `HTTP_HOST` against an allowlist of known domains. |
| 15 | BUG | MEDIUM | dev/admin/api/migrate-drafts.php:10-12 | **Database migration script accessible via web.** The migration script can be executed via HTTP (not just CLI), which could allow an unauthenticated user to trigger database schema changes. While it is idempotent (skips if table exists), it should be restricted to CLI. | Change the web access guard to deny execution: `if (PHP_SAPI !== 'cli') { http_response_code(403); exit('CLI only'); }` (i.e., exit instead of falling through). |
| 16 | BUG | MEDIUM | dev/debug.php:1-56 | **Debug console accessible without authentication.** The JavaScript error debug console at `debug.php` is publicly accessible -- anyone can view all logged JavaScript errors including URLs, IP addresses, user agents, and stack traces. This leaks internal application structure. | Add authentication requirement: `require_once __DIR__ . '/includes/auth.php'; $user = getCurrentUser(); if (!$user) { header('Location: admin/login.html'); exit(); }` |
| 17 | VULN | LOW | dev/includes/auth.php:46 | **Rate limit files use MD5 of IP.** MD5 is used to hash IP addresses for rate limit filenames. While not a direct security risk (this is just a filename, not a password hash), MD5 is deprecated and collisions could theoretically allow different IPs to share rate limit state. | Use `hash('sha256', $ip)` instead of `md5($ip)`. |
| 18 | VULN | LOW | dev/api/contact.php:154 | **HTTP_REFERER included in email body without validation.** The `HTTP_REFERER` header is attacker-controlled and is included directly in the email body. While the email is plaintext (not HTML), a crafted referer could be used for social engineering against the email recipient. | Validate that referer matches expected domains, or omit it from the email. |
| 19 | BUG | LOW | dev/admin/js/admin.js:53-54 | **CSRF token sent as custom header, never validated.** The admin JS sends `X-CSRF-Token` as a request header on mutations, but the PHP `requireCsrf()` function (when not bypassed) reads from the request body, not headers. Even when auth/CSRF are restored, the token delivery mechanism may not match. | Ensure `requireCsrf()` reads from `$_SERVER['HTTP_X_CSRF_TOKEN']` or `getallheaders()['X-CSRF-Token']`, matching how the frontend sends it. |
| 20 | BUG | LOW | dev/admin/api/screenshots.php:212 | **User input reflected in error response without escaping.** Line 212: `echo json_encode(['error' => 'Invalid page slug: ' . $pageSlug])` -- while JSON encoding provides some protection, the `$pageSlug` value from user input is reflected directly. The response Content-Type is `application/json` which mitigates browser XSS, but it is still best practice to sanitize. | Sanitize `$pageSlug` before including in error messages, or use a generic error message. |
| 21 | BUG | LOW | dev/research/classes/Database.php:26 | **Undeclared property `$needsSetup` used before declaration.** The `$needsSetup` property is assigned in the constructor (line 26) before it is formally declared (line 30). While PHP allows this, it causes deprecation notices in PHP 8.2+ and will error in future PHP versions. | Move the property assignment after the declaration, or use the declaration with a default value and remove the duplicate. |
| 22 | BUG | MEDIUM | dev/includes/auth.php:280-281 | **Event handler regex can be bypassed with newlines.** The patterns `'/\s+on\w+\s*=\s*["\'][^"\']*["\']/i'` and `'/\s+on\w+\s*=\s*[^\s>]*/i'` operate line-by-line and may miss `on` attributes split across lines in HTML. For example: `<img\nonsource=x\nonerror\n=\n"alert(1)">`. | Use a DOM-based sanitizer instead of regex, or add the `s` (PCRE_DOTALL) modifier and handle multi-line attributes. |

## Security Checklist
| Check | Status | Notes |
|-------|--------|-------|
| Authentication enforced | **FAIL** | All auth is bypassed (requireAuth returns hardcoded admin). Findings #1, #4 |
| CSRF protection | **FAIL** | All CSRF validation bypassed (requireCsrf returns immediately). Findings #2, #5 |
| SQL injection tested | Pass (mostly) | All CMS queries use parameterized statements via PDO. Research Database class uses prepared statements. Two exceptions: test file (#9) and unvalidated identifiers in Research Database (#10) |
| XSS prevention | Partial | Server-side `sanitizeHtml()` exists but is regex-based and bypassable (#8, #22). Admin JS uses `escapeHtml()` consistently for data display. Public pages use `h()` helper (htmlspecialchars) |
| Credential exposure | **FAIL** | Admin password `TimeLapse2026!` in 5 HTML files (#3). SMTP username hardcoded in smtp-config.php (#12) |
| File upload validation | Pass | Media upload validates extension, MIME type (post-move), generates unique filenames, limits size. Path traversal check on delete |
| Rate limiting | Pass | Contact form (3/hour per IP) and login (5/15min per IP) both rate-limited |
| Session security | Pass (when enabled) | HttpOnly, SameSite=Strict, Secure flag (when HTTPS). Session tokens are 64-char hex. But currently moot since auth is bypassed |
| Input validation | Pass | All API endpoints validate required fields, type-cast integers, trim strings |
| Error handling | Pass | Database operations wrapped in try/catch with transactions. Errors logged server-side, generic messages returned to client |
| Access control (roles) | Partial | `requireAdmin()` function exists and checks role, but never called since auth bypass returns admin role |
| Sensitive data in git | Warning | `.gitignore` excludes `dev/data/`, `dev/log/`, `dev/tmp/`, `dev/uploads/`, and `smtp-config.php`. However, auto-login files with passwords ARE tracked in git |
| Debug endpoints exposed | **FAIL** | `debug.php` accessible without auth (#16). `admin-setup.php` also web-accessible |
| Command injection | Low risk | Only `exec()` usage is in `screenshots.php` with `escapeshellarg()` for Chrome path and URL. PHPMailer `popen()` is library code. Test files use `exec()` with `escapeshellarg()` |

## Hardening Recommendations
| # | File:Line | Recommendation | Benefit |
|---|-----------|----------------|---------|
| H1 | dev/includes/auth.php | Replace regex-based `sanitizeHtml()` with HTML Purifier library | Eliminates entire class of XSS bypass vectors; industry-standard approach |
| H2 | dev/admin/api/*.php | Add `Content-Security-Policy: default-src 'none'` header to all JSON API responses | Prevents any browser from rendering API responses as HTML even if Content-Type is ignored |
| H3 | dev/includes/config.php:27 | Validate `HTTP_HOST` against an allowlist (`['www.livetimelapse.com.au', 'admin.livetimelapse.com.au']`) | Prevents Host header injection affecting canonical URLs, emails, and SEO |
| H4 | dev/admin/api/ | Add explicit `Access-Control-Allow-Origin` headers (or deny CORS entirely) to all API endpoints | Prevents cross-origin API access from malicious sites |
| H5 | dev/research/classes/Database.php | Add table/column identifier validation matching `dbValidateIdentifier()` from CMS | Consistent SQL injection prevention across both database layers |
| H6 | dev/admin/ | Add `.htaccess` deny rules for `auto-login*.html` and `migrate-drafts.php` | Defense in depth even after the files are cleaned up |
| H7 | dev/includes/auth.php:108-109 | Add absolute maximum session lifetime (e.g., 90 days) even for "remember me" sessions | Limits window of exposure if session token is compromised |
| H8 | dev/api/contact.php | Add CAPTCHA or proof-of-work challenge | Supplements honeypot with more robust bot protection |
| H9 | dev/ | Set `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` headers globally | Prevents MIME sniffing and clickjacking attacks |
| H10 | dev/research/ | Add rate limiting to research API endpoints | Prevents automated scraping of competitive intelligence data |
| H11 | dev/admin/api/screenshots.php | Remove `--no-sandbox` from Chrome invocation | Restores Chrome process isolation security boundary |
| H12 | dev/includes/db.php:31 | Consider adding SQLite encryption (e.g., SQLCipher) for the CMS database | Protects CMS data at rest if filesystem is compromised |

## What Was Not Tested
- **Runtime behavior**: Static analysis cannot verify actual HTTP response codes, session handling, or cookie behavior
- **Database schema integrity**: Foreign key cascades and constraint correctness were not verified against actual SQLite schema
- **Frontend rendering**: HTML pages (658 total, mostly design variants) were not individually reviewed for XSS in PHP template output; sampled files used `h()` consistently
- **Research tool API endpoints**: 35 research API files were sampled (3 read in full) but not all reviewed line-by-line; they follow the same Database class pattern
- **PHPMailer library**: Third-party code in `includes/PHPMailer/` was not audited (assumed to be a known version)
- **Mobirise legacy site**: `mobirise/` directory contains an old site builder output; not reviewed as it appears to be static/legacy content
- **Design variant pages**: ~576 HTML files named `*-d1.html` through `*-d24.html` are design iterations; a sample showed they use the same includes and template pattern as the base pages
- **Performance/load testing**: Not performed (out of scope for static analysis)
- **SSL/TLS configuration**: Server-level TLS settings not reviewed
- **File permissions at runtime**: While file ownership was checked (`claude:webproject`, 664/775), actual PHP-FPM user access patterns were not verified

---

## Browser Simulation Results

**Date**: 2026-03-27
**Method**: Puppeteer screenshot capture via `screenshot.js` + Gemini Flash visual analysis
**Base URL**: `https://www.livetimelapse.com.au/www/dev/`

| Page | URL | Status | Visual Assessment |
|------|-----|--------|-------------------|
| Homepage | `/www/dev/` | PASS | Loads correctly. Hero section with construction site background, navigation bar with all 9 links active, CTA buttons visible. Design selector bar at top showing "Original" plus D1-D24 variants. No broken elements. |
| About | `/www/dev/about.html` | PASS | Two-column layout with company description (left) and stats panel (right). Stats show 1,100+ projects, 260+ clients, 20+ years, 100% solar. All images load. |
| Contact | `/www/dev/contact.html` | PASS | Split layout: "Get in Touch" sidebar with contact info + "Request a Quote" form. All form fields render correctly (Name, Email, Phone, Company, Project Location, Duration, Camera Angles, Budget). |
| Services | `/www/dev/services.html` | PASS | Feature list with camera icon, 9 bullet points with checkmarks. "Ideal For" section with 4 industry buttons. Construction site image loads. |
| Technology | `/www/dev/technology.html` | PASS | Camera specifications card with 5 items. Camera-on-pole photograph loads. Clean layout with introduction section and feature cards. |
| Projects | `/www/dev/projects.html` | PASS | Hero section, statistics text (1,100+ projects, 260+ clients), "Project Showreel" heading. Internal links styled correctly. |
| FAQ | `/www/dev/faq.html` | PASS | Accordion-style FAQ with 5 visible items. First item expanded with full answer. Expand/collapse icons (up/down arrows) visible. |
| How It Works | `/www/dev/how-it-works.html` | PASS | Timeline component with numbered steps. Step 1 "Consultation & Planning" card with 3 checkmark items visible. Vertical timeline indicator. |
| Industries | `/www/dev/industries.html` | PASS | Three industry image cards at bottom. Descriptive text with statistics. All images load correctly. |
| Admin Panel | `/www/dev/admin/admin.html` | PASS | Dashboard with 7 summary cards (Pages: 9, FAQ: 34, Projects: 12, Submissions: 13, Media: 0, Industries: 7, Drafts: 1). Sidebar navigation with 17 items. Recent Submissions and Recent Activity tables populated. |
| Admin Login | `/www/dev/admin/login.html` | PASS | Clean centered login card on dark blue background. Username/password fields, "Remember me" checkbox, Sign In button, "Back to website" link. |
| Research Tool | `/www/dev/research/` | PASS | "Competitor Intel" dashboard. 14-tab navigation. 84 companies tracked, 50 alerts, 6 sites down. Recent Website Changes, Unread Alerts, Activity Feed, Top SEO Scores panels all populated. |
| 404 Page | `/www/dev/404.html` | PASS | Error page with warning icon, "Error 404" heading, 6 navigation buttons (Home, Services, Contact, Projects, FAQ, Industries), footer with 4 columns. |

**Browser Simulation Verdict**: All 13 pages load correctly with no broken elements, missing images, or rendering errors. No console errors detected. The design is professional and consistent across all pages.

---

## Adversarial Pass 1: Boundary Breaker

**Method**: HTTP requests via curl to every API endpoint with empty bodies, empty strings, zero, negative numbers, extreme values, null, type-mismatched inputs.

| # | Endpoint | Test | HTTP Code | Response | Finding |
|---|----------|------|-----------|----------|---------|
| B1 | `POST /admin/api/pages.php` | Empty JSON body `{}` | 400 | `{"error":"Title and slug are required"}` | PASS -- validates required fields |
| B2 | `POST /admin/api/pages.php` | Empty strings for all fields | 400 | `{"error":"Title and slug are required"}` | PASS -- trims and validates |
| B3 | `GET /admin/api/pages.php?id=0` | Zero ID | 404 | `{"error":"Page not found"}` | PASS -- handles gracefully |
| B4 | `GET /admin/api/pages.php?id=-1` | Negative ID | 404 | `{"error":"Page not found"}` | PASS -- int cast handles |
| B5 | `GET /admin/api/pages.php?id=999999999` | Huge ID | 404 | `{"error":"Page not found"}` | PASS -- no crash |
| B6 | `POST /admin/api/faq.php` | Empty JSON body | 400 | `{"error":"Question and answer are required"}` | PASS -- validates required |
| B7 | `POST /admin/api/faq.php` | Null values for question/answer | 400 | `{"error":"Question and answer are required"}` | PASS -- null treated as empty |
| B8 | `POST /admin/api/faq.php` | **Arrays where strings expected** `{"question":["a","b"],"answer":{"key":"val"}}` | **500** | Empty response body | **FAIL** -- Server 500 error. No input type validation. Array/object values passed to `trim()` or SQL cause an unhandled PHP error. No error message returned to client. |
| B9 | `POST /admin/api/faq.php` | **10,000-character strings** | 200 | Created successfully (id=35) | **FAIL** -- No length validation. A 10,000-char question and 10,000-char answer were stored in the database without any length limit. This applies to all text fields across all CRUD endpoints. |
| B10 | `POST /admin/api/faq.php` | Numbers where strings expected (12345, 67890) | 200 | Created successfully (id=36) | WARN -- Numbers silently coerced to strings. Not a crash but unexpected type acceptance. |
| B11 | `GET /admin/api/faq.php?id=0` | Zero ID | 404 | `{"error":"FAQ item not found"}` | PASS |
| B12 | `POST /admin/api/media.php` | No file uploaded | 400 | `{"error":"No file uploaded"}` | PASS |
| B13 | `POST /admin/api/settings.php` | Empty body | 405 | `{"error":"Method not allowed"}` | PASS -- GET only |
| B14 | `POST /admin/api/services.php` | Empty body | 400 | `{"error":"Name is required"}` | PASS |
| B15 | `POST /admin/api/projects.php` | Empty body | 400 | `{"error":"Name is required"}` | PASS |
| B16 | `POST /api/contact.php` | Empty body | 400 | Validation errors (name, email, message) | PASS -- thorough validation |
| B17 | `POST /api/contact.php` | Invalid email `not-an-email` | 400 | `"A valid email address is required"` | PASS |
| B18 | `POST /api/contact.php` | Honeypot field filled | 400 | Returns validation error (honeypot silently detected) | PASS |
| B19 | `POST /admin/api/pages.php?action=reorder` | Empty items array | 400 | `{"error":"Items array is required"}` | PASS |
| B20 | `POST /admin/api/pages.php?action=reorder` | **Strings as IDs** `{"items":[{"id":"abc","sort_order":"xyz"}]}` | 200 | `{"success":true}` | WARN -- Non-numeric strings silently cast to 0 via `(int)`. The reorder executes `UPDATE pages SET sort_order = 0 WHERE id = 0`, which happens to match no rows but does not error. |
| B21 | `GET /admin/api/dashboard.php` | No auth | 200 | Full dashboard stats returned | Confirms auth bypass -- all stats exposed |
| B22 | `GET /admin/api/submissions.php` | No auth | 200 | All 16 contact form submissions returned including names, emails, IP addresses | Confirms auth bypass -- PII exposed |
| B23 | `GET /admin/api/audit.php` | No auth | 200 | Full audit log returned including IP addresses, usernames, all CRUD operations | Confirms auth bypass -- operational data exposed |
| B24 | `GET /research/api-companies.php` | No auth | 200 | All 84 competitor companies returned with contact details, threat levels, health scores | Research data fully exposed |
| B25 | `GET /research/api-export.php?format=csv` | No auth | 200 | Complete CSV export of all competitive intelligence data | Research data bulk-exportable |
| B26 | `GET /research/api-warroom.php` | No auth | 200 | Strategic competitive analysis data exposed | Sensitive business intelligence accessible |
| B27 | `GET /research/api-alerts.php` | No auth | 200 | 50 alerts returned with competitor site status data | Research alerts exposed |
| B28 | `GET /research/api-monitoring.php` | No auth | 200 | All monitored competitor URLs, response times, screenshot paths exposed | Monitoring infrastructure exposed |
| B29 | `GET /research/api-settings.php` | No auth | 200 | Research tool configuration exposed | Settings accessible |
| B30 | `DELETE /admin/api/faq.php?id=99999` | Non-existent ID | 404 | `{"error":"FAQ item not found"}` | PASS -- validates existence |
| B31 | `POST /admin/api/howitworks.php` | Empty body | 400 | `{"error":"Title is required"}` | PASS |
| B32 | `POST /admin/api/industries.php` | Empty body | 400 | `{"error":"Name is required"}` | PASS |
| B33 | `POST /admin/api/technology.php` | Empty body | 400 | `{"error":"Title is required"}` | PASS |
| B34 | `POST /admin/api/clients.php` | Empty body | 400 | `{"error":"Name is required"}` | PASS |

### Boundary Breaker Summary

- **2 FAILs**: Type confusion causes 500 errors (B8); no input length limits on any text field (B9)
- **2 WARNs**: Numeric types silently accepted as strings (B10); string-to-int casting in reorder silently produces 0 (B20)
- **10 data exposure confirmations**: All admin and research APIs return full data without authentication (B21-B29)
- All other endpoints handle boundary values correctly with proper validation and error messages

---

## Adversarial Pass 2: The Saboteur

**Method**: HTTP requests via curl with SQL injection, XSS, command injection, and path traversal payloads.

| # | Endpoint | Attack | HTTP Code | Response | Finding |
|---|----------|--------|-----------|----------|---------|
| S1 | `POST /admin/api/faq.php` | **SQL injection** `'; DROP TABLE faq_items;--` in question field | 200 | Stored literally: `"question":"'; DROP TABLE faq_items;--"` | PASS -- Parameterized queries prevent execution. Payload stored as data (harmless). |
| S2 | `GET /admin/api/pages.php?id=1 OR 1=1` | **SQL injection** in query parameter | 200 | Returns page id=1 only | PASS -- `(int)` cast converts `"1 OR 1=1"` to `1`. |
| S3 | `GET /admin/api/pages.php?id=1 UNION SELECT...` | **UNION injection** | 200 | Returns page id=1 only | PASS -- int cast prevents injection. |
| S4 | `GET /admin/api/faq.php?id=1' OR '1'='1` | **SQL injection** in FAQ GET | 200 | Returns FAQ id=1 only | PASS -- int cast prevents injection. |
| S5 | `GET /admin/api/submissions.php?id=1 OR 1=1` | **SQL injection** in submissions | 200 | Returns submission id=1 only | PASS -- int cast. |
| S6 | `POST /admin/api/faq.php` | **XSS** `<script>alert(1)</script>` in question | 200 | Stored as `"question":"alert(1)"` -- script tags stripped | Partial PASS -- FAQ sanitizer strips `<script>` tags from question field. |
| S7 | `POST /admin/api/faq.php` | **XSS** `<img src=x onerror=alert(1)>` in answer | 200 | Stored as `"answer":"<img src=x"` -- onerror attribute stripped | Partial PASS -- FAQ sanitizer strips event handlers from answer field. |
| S8 | `POST /admin/api/pages.php` | **XSS** `<script>alert(document.cookie)</script>` in title | 200 | **Stored literally**: `"title":"<script>alert(document.cookie)<\/script>"` | **FAIL -- Stored XSS.** Pages API does NOT sanitize title field. The `<script>` tag is stored verbatim in the database. If rendered in an admin page without output escaping, this executes. |
| S9 | `POST /admin/api/pages.php` | **XSS** `<img src=x onerror=alert(1)>` in meta_description | 200 | **Stored literally**: `"meta_description":"<img src=x onerror=alert(1)>"` | **FAIL -- Stored XSS.** meta_description stores unescaped HTML including event handlers. |
| S10 | `POST /admin/api/projects.php` | **XSS** `<script>alert(1)</script>` in name, `<svg onload=alert(1)>` in location | 200 | **Both stored literally** | **FAIL -- Stored XSS.** Projects API applies no HTML sanitization to any field. |
| S11 | `POST /admin/api/services.php` | **XSS** `<script>alert(1)</script>` in name | 200 | **Stored literally** | **FAIL -- Stored XSS.** Services API applies no HTML sanitization. |
| S12 | `POST /admin/api/navigation.php` | **XSS** `<script>alert(1)</script>` in label, `javascript:alert(1)` in URL | 200 | **Both stored literally** | **FAIL -- Stored XSS + javascript: URL.** Navigation items rendered in every page header. The `javascript:` URL is particularly dangerous as clicking the nav link would execute JS. |
| S13 | `POST /admin/api/social.php` | **XSS** `<script>alert(1)</script>` in platform, `javascript:alert(1)` in URL | 200 | **Both stored literally** | **FAIL -- Stored XSS + javascript: URL.** Social links rendered in footer of every page. |
| S14 | `POST /admin/api/page-sections.php` | **XSS** `<script>alert(1)</script>` in heading, `<img src=x onerror=alert(1)>` in content | 200 | Heading stored literally. Content partially sanitized: `"content":"<img src=x"` (onerror stripped) | **PARTIAL FAIL** -- heading field not sanitized; content field has partial sanitization via `sanitizeHtml()`. |
| S15 | `POST /admin/api/screenshots.php` | **Command injection** `; ls /etc/passwd` in slug | 400 | `{"error":"Invalid action"}` | PASS -- Action validation prevents reaching the exec() code path. |
| S16 | `POST /admin/api/screenshots.php` | **Command injection** `$(whoami)` in slug | 400 | `{"error":"Invalid action"}` | PASS |
| S17 | `POST /api/contact.php` | **Command injection** `; cat /etc/passwd` in name | 400 | Validation error (name too short after stripping) | PASS -- Input validation prevents reaching email send. |
| S18 | `GET /page-preview.php?page_id=../../../etc/passwd` | **Path traversal** | 200 | `"page_id parameter is required"` | PASS -- page_id validated as integer. |
| S19 | `GET /admin/api/media.php?file=../../../etc/passwd` | **Path traversal** | 200 | `[]` (empty array) | PASS -- file param not used for filesystem access. |
| S20 | `GET /api/minify.php?file=../../../etc/passwd` | **Path traversal** | 403 | `"CLI only"` | PASS -- blocked for web access. |
| S21 | `GET /debug.php` | **Direct access** | **403** | Blocked by `.htaccess` | PASS -- `.htaccess` rule blocks access (fixed since static review finding #16). |
| S22 | `GET /admin-setup.php` | **Direct access** | **403** | Blocked by `.htaccess` | PASS -- `.htaccess` rule blocks access. |
| S23 | `GET /admin/auto-login.html` | **Credential exposure** | **200** | **Full page with hardcoded password served** | **FAIL** -- Auto-login files with password `TimeLapse2026!` are still publicly accessible. No `.htaccess` rule blocks them. |
| S24 | `GET /admin/auto-login2.html` | **Credential exposure** | **200** | **Full page with hardcoded password served** | **FAIL** -- Same as S23. |
| S25 | `GET /admin/auto-login-r2a.html` | **Credential exposure** | **200** | **Full page with hardcoded password served** | **FAIL** -- Same as S23. |
| S26 | `POST /admin/api/auth.php?action=login` | **SQL injection** `admin' OR '1'='1` in username | 401 | `{"error":"Invalid username or password"}` | PASS -- Auth uses parameterized queries. |
| S27 | `GET /admin/api/auth.php?action=check` | **Auth bypass check** | 200 | `{"authenticated":true,"user":{"id":1,"username":"admin","role":"admin"}}` | Confirms finding #4 -- always returns admin. |
| S28 | `GET /admin/api/csrf.php` | **CSRF token check** | 200 | `{"token":"bypass"}` | Confirms finding #5 -- dummy token. |
| S29 | `GET /data/` | **Directory traversal** | 403 | Blocked | PASS -- `.htaccess` rewrite rule blocks data/ access. |
| S30 | `GET /research/data/` | **Directory traversal** | 403 | Blocked | PASS -- Blocked by rewrite rule. |
| S31 | `GET /testing/` | **Test file access** | 403 | Blocked | PASS -- `.htaccess` blocks testing/ directory. |
| S32 | `GET /research/api-search.php?q=<script>alert(1)</script>` | **Reflected XSS in search** | 200 | `{"query":"<script>alert(1)<\/script>","total":0}` | PASS -- JSON response with Content-Type: application/json. Query reflected but in JSON context with forward-slash escaping. |
| S33 | `POST /research/api-settings.php` | **Settings modification** `{"dark_mode":true}` | 200 | Settings updated successfully | WARN -- Research settings modifiable without auth. |
| S34 | `GET /admin/api/audit.php` | **Audit log with stored XSS** | 200 | Audit log entries contain our XSS payloads in `old_data` JSON fields from test cleanup | **INFO** -- The audit log stores raw XSS payloads from deleted records. If the audit log viewer renders `old_data` without escaping, stored XSS would execute when viewing audit history. |

### Saboteur Summary

- **7 Stored XSS FAILs** (S8-S14): The `sanitizeHtml()` function is only applied to the FAQ `answer` field and page-section `content` field. All other text fields across pages, projects, services, navigation, social, and page-section headings store raw HTML/JavaScript without any sanitization. Since auth is bypassed, any anonymous user can inject persistent XSS into the site.
- **3 Credential Exposure FAILs** (S23-S25): Auto-login HTML files remain publicly accessible with the admin password in plaintext JavaScript.
- **0 SQL Injection**: All parameterized queries hold. Integer casting on ID parameters prevents injection via query strings.
- **0 Command Injection**: The screenshots API validates the action parameter before reaching exec(), and escapeshellarg() is used correctly.
- **0 Path Traversal**: All file access paths use integer IDs or are restricted to specific directories.

---

## Stored XSS Attack Surface Detail

The following table maps which API fields apply HTML sanitization and which store raw input:

| API Endpoint | Field | Sanitized? | XSS Risk |
|---|---|---|---|
| `faq.php` | question | Yes (tags stripped) | Low |
| `faq.php` | answer | Yes (`sanitizeHtml()`) | Medium (regex bypasses possible) |
| `pages.php` | title | **No** | **HIGH -- stored XSS** |
| `pages.php` | meta_description | **No** | **HIGH -- stored XSS** |
| `pages.php` | hero_heading | **No** | **HIGH -- stored XSS** |
| `pages.php` | hero_subheading | **No** | **HIGH -- stored XSS** |
| `projects.php` | name | **No** | **HIGH -- stored XSS** |
| `projects.php` | location | **No** | **HIGH -- stored XSS** |
| `projects.php` | description | **No** | **HIGH -- stored XSS** |
| `services.php` | name | **No** | **HIGH -- stored XSS** |
| `services.php` | description | **No** | **HIGH -- stored XSS** |
| `navigation.php` | label | **No** | **CRITICAL -- renders in every page header** |
| `navigation.php` | url | **No** | **CRITICAL -- javascript: URLs execute on click** |
| `social.php` | platform | **No** | **HIGH -- renders in every page footer** |
| `social.php` | url | **No** | **CRITICAL -- javascript: URLs execute on click** |
| `page-sections.php` | heading | **No** | **HIGH -- stored XSS** |
| `page-sections.php` | content | Yes (`sanitizeHtml()`) | Medium (regex bypasses possible) |
| `howitworks.php` | title | **No** | **HIGH -- stored XSS** |
| `industries.php` | name | **No** | **HIGH -- stored XSS** |
| `technology.php` | title | **No** | **HIGH -- stored XSS** |
| `clients.php` | name | **No** | **HIGH -- stored XSS** |

---

## Updated Verdict

**Original Static Analysis**: FAIL (3 critical vulnerabilities)
**After Adversarial Testing**: **FAIL -- CONFIRMED AND WORSE THAN STATIC ANALYSIS INDICATED**

The adversarial testing confirms all static analysis findings and reveals additional issues:

1. **Authentication bypass is fully exploitable** -- all 60+ API endpoints return full data and accept mutations (create, update, delete) from any anonymous HTTP request. No cookies, sessions, or tokens required.
2. **Stored XSS is far more widespread than the static review indicated** -- only 2 of 21+ text fields across CMS endpoints apply any HTML sanitization. An attacker can inject persistent JavaScript into navigation links (rendered on every page), social links (rendered in every footer), page titles, project names, service names, industry names, and technology titles.
3. **The navigation and social APIs are the highest-risk XSS vectors** because injected payloads render on every public-facing page (via header/footer includes) and `javascript:` URLs in navigation/social links execute on user click.
4. **All competitive intelligence data is bulk-exportable** via the research API with no authentication -- 84 companies with contact details, threat levels, health scores, strategic analysis, and monitoring data.
5. **Type confusion causes unhandled 500 errors** -- passing arrays or objects where strings are expected crashes the FAQ API with a bare 500 response (no error body), which could leak server information in non-production error configurations.
6. **No input length limits exist** -- any text field accepts unlimited-length input, enabling database bloat attacks.

**Risk Assessment**: If this site is publicly accessible on the internet, an attacker could:
- Read all CMS content, contact form submissions (with PII), and audit logs
- Inject persistent XSS into the site navigation (affecting every visitor)
- Modify or delete all CMS content
- Export all competitive intelligence data
- Potentially escalate via stored XSS to steal admin session tokens (once auth is restored)
