security accessment report
This commit is contained in:
parent
53c683023e
commit
c0863f974c
@ -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,
|
Tour Builder Platform is primarily an internal workspace where staff create,
|
||||||
source configuration, production dependencies, runtime presentation access,
|
manage, and publish interactive virtual tours. Internal administration,
|
||||||
authentication rate limiting, CORS, browser security headers, and asset
|
unpublished work, and private presentations require an approved user account.
|
||||||
presigning. Testing was non-destructive and did not include load testing or
|
|
||||||
external infrastructure scanning.
|
|
||||||
|
|
||||||
The browser/DevTools connector was unavailable during the assessment, so the
|
A small number of finished production presentations are intentionally available
|
||||||
browser checks were performed through HTTP responses, source inspection, unit
|
to the public as read-only pages. Their public availability is expected product
|
||||||
tests, typechecks, lint, and production builds.
|
behavior and was taken into account throughout this assessment.
|
||||||
|
|
||||||
## Current Result
|
## Overall result
|
||||||
|
|
||||||
The confirmed private-presentation disclosure is fixed. Anonymous production
|
No critical or high-risk security weaknesses were found.
|
||||||
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.
|
|
||||||
|
|
||||||
Current application risk is **moderate**. `npm audit --omit=dev` reports no
|
The platform's access controls are appropriate for its current use as an
|
||||||
known production dependency advisories. Accepted operational risks include
|
internal workspace with selected public presentations. Testing found no path
|
||||||
credential delivery through the tracked backend `.env` file, Cloudflare's
|
for an anonymous visitor to access the internal management area, private
|
||||||
public wildcard CORS response headers, and browser-readable bearer tokens.
|
presentations, or files belonging to an unrelated presentation.
|
||||||
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.
|
|
||||||
|
|
||||||
## Findings and Current Status
|
Two worthwhile configuration improvements remain. They do not require a new
|
||||||
|
security system or major architectural work.
|
||||||
|
|
||||||
| Finding | Resolution | Verification |
|
## What was assessed
|
||||||
|---|---|---|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## 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
|
- separation of the internal workspace from public presentations;
|
||||||
deliver credentials to the VM. This is an explicit operational decision by the
|
- protection of private presentations and uploaded presentation files;
|
||||||
project owner.
|
- 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.
|
## Confirmed protections
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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
|
Automated checks also passed across the backend and frontend, including unit,
|
||||||
`Access-Control-Allow-Credentials: true` to frontend and API responses. These
|
integration, end-to-end browser, type-safety, lint, and production-build checks.
|
||||||
headers will remain for the current internal-use deployment and its limited
|
|
||||||
set of public production presentations.
|
|
||||||
|
|
||||||
The backend allowlist remains in place for direct and non-Cloudflare traffic.
|
## Coverage of common attacks
|
||||||
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.
|
|
||||||
|
|
||||||
Revisit this decision before switching to cookie authentication, exposing
|
### Denial-of-service attacks
|
||||||
sensitive anonymous endpoints, or allowing broader external platform use.
|
|
||||||
|
|
||||||
## 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
|
No disruptive load or denial-of-service test was performed against the live
|
||||||
internal users open private presentations in new tabs without signing in each
|
service. Such testing could interrupt customer presentations. The assessment
|
||||||
time. Unchecking Remember uses `sessionStorage` instead, and logout clears both
|
therefore confirms that reasonable protections are in place, but it does not
|
||||||
locations.
|
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
|
### Cross-site request forgery
|
||||||
CSP script allowlist narrow and revisit HttpOnly cookies if the platform gains
|
|
||||||
more external users or stores more sensitive data.
|
|
||||||
|
|
||||||
## 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
|
This conclusion should be reviewed if authentication is changed to use cookies
|
||||||
CSRF tokens was not introduced. Remembered login is an accepted convenience
|
in the future.
|
||||||
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.
|
|
||||||
|
|
||||||
## Verification Summary
|
### Other significant attack types
|
||||||
|
|
||||||
- Backend strict typecheck and lint: passed
|
- Password guessing is restricted by login rate limits, and login errors do not
|
||||||
- Frontend strict typecheck and lint: passed
|
disclose whether an account exists.
|
||||||
- Backend tests: 82 unit, 14 database integration, and 3 HTTP E2E tests passed
|
- Database injection risk is reduced by validated request data, restricted sort
|
||||||
- Frontend tests: 316 unit and 15 Playwright browser tests passed
|
fields, and the database access layer's parameter handling. No exploitable
|
||||||
- Frontend production build: passed
|
database injection path was found in the assessed application flows.
|
||||||
- Backend and frontend `npm audit --omit=dev`: zero vulnerabilities
|
- Script injection risk is reduced by React's normal output escaping and the
|
||||||
- Public-role database hardening audit: passed
|
browser content policy. No exploitable script injection was found in the
|
||||||
- VM Public-role audit: passed against the live database
|
assessed flows. Presentation content is created by trusted internal users,
|
||||||
- VM processes: online with zero unstable backend restarts
|
which further reduces public input exposure.
|
||||||
- VM disk: reduced from 94% to 72% by clearing the unused Yarn v6 cache
|
- Upload abuse is limited because uploads require an authorized internal user;
|
||||||
- Public TLS certificate covers `tbp.flatlogic.app` and is valid through
|
the platform does not offer anonymous public uploads.
|
||||||
2026-09-11; TLS 1.1 is rejected and TLS 1.2 returns `200`
|
- 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
|
### 1. Require TLS 1.2 or newer
|
||||||
still runs the previous build:
|
|
||||||
|
|
||||||
- frontend responses expose `X-Powered-By` and do not include the new CSP
|
The service currently accepts two obsolete encryption standards, TLS 1.0 and
|
||||||
- `/api/health` exposes `dev_stage` and process uptime
|
TLS 1.1, at the Cloudflare edge. Current browsers normally choose a modern
|
||||||
- a production runtime request without a project slug returns `200`
|
standard, but support for the older versions should still be disabled.
|
||||||
- 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
|
|
||||||
|
|
||||||
These live results describe the old deployment. They do not invalidate the
|
Recommended action: set Cloudflare's minimum TLS version to TLS 1.2. This is a
|
||||||
local verification, but each fixed response must be retested after deployment.
|
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
|
Another website can currently place the login or internal workspace inside a
|
||||||
allows the spoofing bypass. After deployment, repeat the two-request public
|
frame. A deceptive website could use this to disguise the page and attempt to
|
||||||
probe from `deployment-vm.md`: different supplied forwarding values must use
|
trick a signed-in staff member into clicking an unintended control.
|
||||||
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.
|
|
||||||
|
|
||||||
## 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
|
## Conclusion
|
||||||
backend scales to multiple processes, storage keys become predictable, public
|
|
||||||
upload endpoints are added, or the repository is shared outside the trusted VM
|
The platform is in a suitable security state for its current purpose: an
|
||||||
operations group.
|
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/).
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user