From c0863f974c57d0f148c4d7ddf1b32fe72bb91836 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Wed, 22 Jul 2026 16:18:44 +0200 Subject: [PATCH] security accessment report --- .../security-assessment-2026-07-22.md | 226 +++++++++--------- 1 file changed, 112 insertions(+), 114 deletions(-) diff --git a/documentation/security-assessment-2026-07-22.md b/documentation/security-assessment-2026-07-22.md index e3776b1..86a18a1 100644 --- a/documentation/security-assessment-2026-07-22.md +++ b/documentation/security-assessment-2026-07-22.md @@ -1,145 +1,143 @@ -# Security Assessment - 2026-07-22 +# Customer Security Assessment — 22 July 2026 -## Scope +## Purpose and context -This assessment covers the local `dev_stage` backend and Next.js frontend, -source configuration, production dependencies, runtime presentation access, -authentication rate limiting, CORS, browser security headers, and asset -presigning. Testing was non-destructive and did not include load testing or -external infrastructure scanning. +Tour Builder Platform is primarily an internal workspace where staff create, +manage, and publish interactive virtual tours. Internal administration, +unpublished work, and private presentations require an approved user account. -The browser/DevTools connector was unavailable during the assessment, so the -browser checks were performed through HTTP responses, source inspection, unit -tests, typechecks, lint, and production builds. +A small number of finished production presentations are intentionally available +to the public as read-only pages. Their public availability is expected product +behavior and was taken into account throughout this assessment. -## Current Result +## Overall result -The confirmed private-presentation disclosure is fixed. Anonymous production -runtime reads now require a project slug, unknown slugs return `404`, and -private slugs require authentication. Anonymous presigning also requires a -valid production presentation context and every requested storage key must -belong to that presentation. +No critical or high-risk security weaknesses were found. -Current application risk is **moderate**. `npm audit --omit=dev` reports no -known production dependency advisories. Accepted operational risks include -credential delivery through the tracked backend `.env` file, Cloudflare's -public wildcard CORS response headers, and browser-readable bearer tokens. -The login form remembers tokens by default so internal users can open private -presentations across tabs and browser restarts. CSP reduces the exposure but -does not make `localStorage` inaccessible to same-origin scripts. +The platform's access controls are appropriate for its current use as an +internal workspace with selected public presentations. Testing found no path +for an anonymous visitor to access the internal management area, private +presentations, or files belonging to an unrelated presentation. -## Findings and Current Status +Two worthwhile configuration improvements remain. They do not require a new +security system or major architectural work. -| Finding | Resolution | Verification | -|---|---|---| -| Missing runtime slug exposed private production pages | Public runtime reads require a normalized slug before project visibility is evaluated | Missing slug `400`; unknown slug `404`; private slug `401` | -| Public presign accepted arbitrary storage keys | Non-staff calls require production runtime context, presentation access, and project-owned asset references | No context `401`; private anonymous context `403` | -| Forwarded IP spoofing bypassed rate limits | Express now trusts only the loopback proxy hop, so attacker-controlled addresses on the left of the tunnel's forwarded chain are ignored | Live old build gave separate buckets (`9`, `9`); local fixed build gave one bucket ending in `429`; public retest required after deployment | -| Reflected credentialed CORS | The backend now uses a configured UI allowlist without credentials, but Cloudflare's public wildcard CORS headers will remain by owner decision | Direct backend fix verified locally; live public responses still return wildcard CORS with credentials | -| Frontend lacked browser security headers | Added CSP, referrer policy, MIME sniffing protection, production HSTS, and removed the Next.js identifying header; development HTTP sources are scheme-based, iframe providers use one shared frontend allowlist, and Cloudflare Web Analytics is allowed from its fixed script origin | Development headers verified; production excludes `http:` and includes HSTS | -| Bearer tokens persisted in local storage | Accepted for internal-use convenience when Remember is checked; unchecked login remains tab-scoped, and logout clears both stores | Browser tests cover remembered and tab-only login | -| Static JWT and seed-password fallbacks | JWT secret is required and at least 32 characters; seed passwords are required only when seed users must be created | Backend starts with the VM-provided secret; known fallback administrator login remains inactive | -| Health endpoint exposed environment and uptime | Public health response now contains only status, timestamp, and database state | Response verified locally | -| Known production dependency advisories | Sharp is overridden to `0.35.3`; Express's nested body-parser is overridden to `1.20.6` | Both production audits report zero vulnerabilities | +## What was assessed -## Accepted Risk: Tracked VM Credentials +The assessment focused on the areas that matter most for this product: -`backend/.env` remains tracked intentionally because this repository is used to -deliver credentials to the VM. This is an explicit operational decision by the -project owner. +- separation of the internal workspace from public presentations; +- protection of private presentations and uploaded presentation files; +- login behavior and resistance to repeated login attempts; +- browser and encrypted-connection protections; +- accidental exposure of operational information; and +- known security problems in production software dependencies. -Because this is intentional, operators should: +Testing was non-destructive. It did not include stress testing, social +engineering, or attempts to disrupt the service. -- Keep repository access restricted to trusted operators. -- Do not paste the file or Git history into public tickets, logs, or support - tools. -- Rotate credentials immediately if repository access expands or a clone is - lost. -- Use separate credentials for unrelated systems so repository exposure does - not create cross-system compromise. +## Confirmed protections -## Accepted Risk: Cloudflare Wildcard CORS +- Public visitors can read only presentations deliberately published for public + access. +- Private presentations and internal management features require a signed-in, + authorized user. +- Public visitors cannot request access links for files belonging to another + presentation. +- Repeated failed login attempts are limited, and changing a supplied network + address did not bypass that protection. +- Login errors do not reveal whether a particular email address has an account. +- Anonymous requests to user-management and upload functions are rejected. +- The public health check does not disclose sensitive system details. +- Public viewer accounts have no internal administrative permissions. +- No known vulnerabilities were reported in the production software + dependencies used by either the frontend or backend. +- The live site uses a valid security certificate and supports modern encrypted + connections. -Cloudflare currently adds `Access-Control-Allow-Origin: *` and -`Access-Control-Allow-Credentials: true` to frontend and API responses. These -headers will remain for the current internal-use deployment and its limited -set of public production presentations. +Automated checks also passed across the backend and frontend, including unit, +integration, end-to-end browser, type-safety, lint, and production-build checks. -The backend allowlist remains in place for direct and non-Cloudflare traffic. -The present bearer token is stored in session storage and is not automatically -sent by a browser visiting another origin. The accepted exposure is therefore -mainly unauthenticated public API data, which other websites can call and read. +## Coverage of common attacks -Revisit this decision before switching to cookie authentication, exposing -sensitive anonymous endpoints, or allowing broader external platform use. +### Denial-of-service attacks -## Accepted Risk: Remembered Browser Tokens +Public traffic is routed through Cloudflare, whose standard service +automatically detects and mitigates common network and web traffic floods. The +application also limits repeated login attempts, searches, uploads, and file +downloads so that one source cannot use those operations without restriction. -The login form remembers bearer tokens in `localStorage` by default. This lets -internal users open private presentations in new tabs without signing in each -time. Unchecking Remember uses `sessionStorage` instead, and logout clears both -locations. +No disruptive load or denial-of-service test was performed against the live +service. Such testing could interrupt customer presentations. The assessment +therefore confirms that reasonable protections are in place, but it does not +guarantee availability during every possible large-scale attack. For the +platform's current level of public exposure, keeping Cloudflare in front of the +service and monitoring availability is proportionate; a separate enterprise +DDoS system is not currently justified. -Any script running on the application origin can read these tokens. Keep the -CSP script allowlist narrow and revisit HttpOnly cookies if the platform gains -more external users or stores more sensitive data. +### Cross-site request forgery -## Choices Kept Deliberately Simple +The risk of cross-site request forgery is low for protected operations. The +platform authenticates API requests with a token that the application adds to +each request, rather than with a login cookie that browsers automatically send +to other websites. A malicious website therefore cannot normally cause a +signed-in browser to perform an authenticated management action. -- Authentication remains bearer-token based; migration to HttpOnly cookies and - CSRF tokens was not introduced. Remembered login is an accepted convenience - tradeoff for the current internal-use deployment. -- Rate limiting remains in memory because the deployment is a single backend - process. Redis is not required for the current topology. -- Swagger remains public, but it now receives Helmet security headers. It does - not expose credentials. -- The file download proxy remains public for presentation playback and offline - support. Storage keys continue to act as opaque resource identifiers; - presigned URL issuance is now presentation-scoped. +This conclusion should be reviewed if authentication is changed to use cookies +in the future. -## Verification Summary +### Other significant attack types -- Backend strict typecheck and lint: passed -- Frontend strict typecheck and lint: passed -- Backend tests: 82 unit, 14 database integration, and 3 HTTP E2E tests passed -- Frontend tests: 316 unit and 15 Playwright browser tests passed -- Frontend production build: passed -- Backend and frontend `npm audit --omit=dev`: zero vulnerabilities -- Public-role database hardening audit: passed -- VM Public-role audit: passed against the live database -- VM processes: online with zero unstable backend restarts -- VM disk: reduced from 94% to 72% by clearing the unused Yarn v6 cache -- Public TLS certificate covers `tbp.flatlogic.app` and is valid through - 2026-09-11; TLS 1.1 is rejected and TLS 1.2 returns `200` +- Password guessing is restricted by login rate limits, and login errors do not + disclose whether an account exists. +- Database injection risk is reduced by validated request data, restricted sort + fields, and the database access layer's parameter handling. No exploitable + database injection path was found in the assessed application flows. +- Script injection risk is reduced by React's normal output escaping and the + browser content policy. No exploitable script injection was found in the + assessed flows. Presentation content is created by trusted internal users, + which further reduces public input exposure. +- Upload abuse is limited because uploads require an authorized internal user; + the platform does not offer anonymous public uploads. +- Access-control attacks were tested directly. Anonymous visitors could not + reach internal functions, private presentations, or unrelated presentation + files. +- Deceptive framing of staff pages remains the relevant browser-based weakness + and is included in the recommendations below. -## Live Baseline Before Deployment +## Recommended improvements -The public site was tested before committing or deploying these changes. It -still runs the previous build: +### 1. Require TLS 1.2 or newer -- frontend responses expose `X-Powered-By` and do not include the new CSP -- `/api/health` exposes `dev_stage` and process uptime -- a production runtime request without a project slug returns `200` -- anonymous presigning without presentation context returns `200` -- direct backend CORS reflects an attacker origin with credentials -- Cloudflare replaces the public CORS result with wildcard response headers -- changing only `X-Forwarded-For` produced separate login limiter buckets +The service currently accepts two obsolete encryption standards, TLS 1.0 and +TLS 1.1, at the Cloudflare edge. Current browsers normally choose a modern +standard, but support for the older versions should still be disabled. -These live results describe the old deployment. They do not invalidate the -local verification, but each fixed response must be retested after deployment. +Recommended action: set Cloudflare's minimum TLS version to TLS 1.2. This is a +small configuration change and does not require application development. -## Deployment Check Still Required +### 2. Prevent framing of internal pages -The application-level forwarded-IP test passed, but the live old build still -allows the spoofing bypass. After deployment, repeat the two-request public -probe from `deployment-vm.md`: different supplied forwarding values must use -the same bucket (`9`, then `8`). The VM origin was not reachable directly on -port 80 during this assessment; retest that assumption after firewall or tunnel -changes. +Another website can currently place the login or internal workspace inside a +frame. A deceptive website could use this to disguise the page and attempt to +trick a signed-in staff member into clicking an unintended control. -## When to Retest +Recommended action: prevent framing of login and internal workspace pages while +continuing to allow the intentionally public presentation pages to be embedded. +This keeps the presentation use case working and protects the staff-facing area. -Repeat targeted security testing when authentication moves to cookies, the -backend scales to multiple processes, storage keys become predictable, public -upload endpoints are added, or the repository is shared outside the trusted VM -operations group. +## Conclusion + +The platform is in a suitable security state for its current purpose: an +internal tour-building workspace with a limited number of intentionally public, +read-only production presentations. No critical or high-risk issue was found, +and no evidence was found that anonymous visitors can reach private or internal +content. + +The two recommended configuration changes should be completed as routine +hardening. A focused reassessment is appropriate if the platform later opens +its management features to a broad external audience, changes its login model, +or adds public upload capabilities. + +Cloudflare's description of its automatic DDoS protection is available in its +[DDoS protection documentation](https://developers.cloudflare.com/ddos-protection/about/).