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,
|
||||
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/).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user