PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-19
Related docs: 15_Security_and_Data_Protection.md, 02_Infrastructure_and_Environments.md, 08_API_Contracts.md, 18_QA_Pipeline_and_Testing.md
This document defines the penetration testing strategy for the Savoy Signature platform. It maps the OWASP Top 10 (2021) to the project’s headless architecture, catalogs every mandatory test, specifies the CI/CD integration points, and establishes the security gate that must pass before any environment goes live.
The headless architecture (Next.js + Umbraco + Cloudflare + Azure) shifts the attack surface compared to a monolithic CMS. The public-facing layer is the CDN and the Node.js SSR app — the CMS backoffice and database are network-isolated. This document addresses both layers.
Middleware (Auth Gate + Routing)
api/auth/validate-backoffice
Endpoint Method Auth Purpose Risk Level /* (SSR pages)GET None (public) / Auth Gate (DEV) Hotel websites Medium /api/webhooks/umbracoPOST HMAC SHA-256 Cache purge trigger High /api/gate/loginPOST Credentials DEV auth gate login High /api/gate/logoutPOST Cookie DEV auth gate logout Low /api/draftGET Secret query param Content preview mode Medium /api/draft/disableGET Secret query param Disable preview Low /api/cloudflare/statsGET API key header/param Dashboard stats proxy Medium /umbraco/delivery/api/v2/*GET API key (optional) Content Delivery API High /umbracoGET/POST Azure AD SSO + MFA CMS backoffice Critical /api/auth/validate-backofficePOST X-Gate-Key header Gate credential check High
Endpoint Access Protection Azure SQL Private endpoint (VNet) TDE, AAD auth Blob Storage Private endpoint (VNet) TDE, SAS tokens Key Vault Managed Identity only RBAC, audit log
Each OWASP category is mapped to the Savoy architecture with specific test procedures.
# Test Target Procedure Severity A01.1 Cross-site data leakage Delivery API Request content without Start-Item header — verify response is empty or error, not cross-site data Critical A01.2 Start-Item spoofing Delivery API Send Start-Item: savoy-palace but request URLs belonging to royal-savoy — must return 404 Critical A01.3 Backoffice public access /umbracoAttempt to load backoffice from public IP (not VPN/office) — must be blocked by Cloudflare Zero Trust or Azure IP restriction Critical A01.4 RBAC bypass Backoffice Login as “Savoy Palace Editor” and attempt to edit content under royal-savoy siteRoot — must be denied High A01.5 Auth Gate bypass DEV middleware Access DEV pages without __dev_gate cookie — must redirect to /gate/login High A01.6 Auth Gate JWT tampering DEV middleware Modify JWT payload (change sub email) without re-signing — must reject High A01.7 Auth Gate expired token DEV middleware Use JWT with exp in the past — must redirect to login Medium A01.8 Preview mode without secret /api/draftRequest /api/draft?secret=wrong&path=/ — must return 401 High A01.9 Webhook without signature /api/webhooks/umbracoPOST without X-Webhook-Signature — must return 401 High A01.10 Stats endpoint without key /api/cloudflare/statsRequest without x-dashboard-key header — must return 401 Medium A01.11 CORS origin bypass Delivery API Send Origin: https://evil.com — response must NOT include Access-Control-Allow-Origin: https://evil.com High A01.12 Direct Azure URL access *.azurewebsites.netAccess Next.js via direct Azure URL — must redirect 301 to Cloudflare domain Medium
# Test Target Procedure Severity A02.1 TLS version All domains Verify TLS 1.2+ only — TLS 1.0/1.1 must be rejected High A02.2 HSTS header All domains Verify Strict-Transport-Security: max-age=63072000; includeSubDomains; preload High A02.3 SSL certificate chain All domains Verify valid chain, no mixed content, no expired certs High A02.4 Weak ciphers All domains Scan for deprecated ciphers (RC4, DES, 3DES, NULL) — must not be present High A02.5 HMAC timing attack Webhook Verify crypto.timingSafeEqual() is used (code review) — already implemented Medium A02.6 JWT algorithm confusion Auth Gate Send JWT with alg: none — must be rejected High A02.7 Secrets in client bundle Frontend JS Analyze Next.js client bundle for leaked env vars (API keys, secrets) — must find none Critical A02.8 Secrets in Docker image Docker Inspect Docker image layers for embedded secrets — must find none High A02.9 Database encryption Azure SQL Verify TDE is enabled on Azure SQL — check via Azure Portal or CLI High A02.10 Blob Storage encryption Azure Blob Verify TDE + HTTPS-only access — public anonymous access must be disabled High
# Test Target Procedure Severity A03.1 XSS via CMS content SSR pages Inject <script>alert(1)</script> in CMS Rich Text field — verify CSP blocks execution High A03.2 XSS via URL parameters SSR pages Inject script in query params (?q=<script>alert(1)</script>) — must not execute High A03.3 Stored XSS via SVG upload Media library Upload SVG with <script> tag — must be sanitized or stripped High A03.4 SQL injection via Delivery API API filters Inject SQL in filter, sort, skip query params — verify EF Core ORM parameterizes High A03.5 Path traversal Start-Item headerSend Start-Item: ../../../etc/passwd — must return error, not file contents High A03.6 Header injection Next.js middleware Inject CRLF in custom headers (x-site-key: savoy\r\nX-Evil: hack) — must not split headers Medium A03.7 JSON injection in webhook Webhook handler Send malformed JSON payload — must return 400, not crash Medium A03.8 Template injection Block List labels Inject {blText: ../../etc/passwd} in Block List label UFM — verify sandboxed parsing Low A03.9 NoSQL / OData injection Delivery API Inject OData operators in filter params — verify API rejects or sanitizes Medium
# Test Target Procedure Severity A04.1 Rate limiting on login /api/gate/loginSend 100 login attempts in 1 minute — verify Cloudflare or app rate limits kick in High A04.2 Rate limiting on forms /api/forms/*Send 20 form submissions in 1 minute — verify 429 response after threshold (5 req/5min per IP) High A04.3 Rate limiting on search /api/searchSend 50 search requests in 1 minute — verify 429 after threshold (20 req/min per IP) Medium A04.4 Account lockout Gate login Send 10 failed login attempts — verify account lockout via ASP.NET Identity High A04.5 Credential enumeration Gate login Test valid email + wrong password vs invalid email — response must be identical (no enumeration) High A04.6 Open redirect /api/gate/login?returnUrl=Send returnUrl=https://evil.com — must redirect to / not to external domain High A04.7 Open redirect via preview /api/draft?path=Send path=https://evil.com — must return 400 High A04.8 Webhook replay attack Webhook Replay a previously captured valid webhook payload — verify idempotency or timestamp check Medium A04.9 Excessive data exposure Delivery API Request content and verify response does NOT include sensitive fields (passwords, internal IDs, editor emails) High
# Test Target Procedure Severity A05.1 Security headers audit All domains Verify presence of: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP High A05.2 CSP policy Next.js Verify Content-Security-Policy header blocks inline scripts (or uses nonces) — current unsafe-inline + unsafe-eval in script-src is a known gap High A05.3 Server information leakage All domains Check Server, X-Powered-By, X-AspNet-Version headers — must not reveal stack details Medium A05.4 Directory listing Azure Blob Attempt to list blob container contents — public listing must be disabled High A05.5 Debug mode All environments Verify NODE_ENV=production in STAGE/QA/PROD — no debug endpoints exposed High A05.6 Default credentials Umbraco Verify Admin1234! (dev default) is NOT used in STAGE/QA/PROD Critical A05.7 TLS_REJECT_UNAUTHORIZED Production Verify NODE_TLS_REJECT_UNAUTHORIZED=0 is NOT set in any deployed environment Critical A05.8 Unnecessary HTTP methods Delivery API Send PUT, DELETE, PATCH to Delivery API — must return 405 Method Not Allowed Medium A05.9 CORS wildcard Delivery API Verify CORS does NOT use Access-Control-Allow-Origin: * — must be specific origins High A05.10 Error page information 500 errors Trigger server errors — verify no stack traces, file paths, or internal details in response Medium A05.11 Cloudflare WAF active All domains Verify WAF Managed Ruleset is enabled and blocking known exploits High A05.12 Permissions-Policy header All domains Verify restrictive Permissions-Policy (camera, microphone, geolocation blocked) Medium
# Test Target Procedure Severity A06.1 npm audit Frontend Run pnpm audit — zero high/critical vulnerabilities High A06.2 NuGet audit Umbraco Run dotnet list package --vulnerable — zero high/critical High A06.3 Docker base image node:20-alpineScan with trivy image — zero critical CVEs High A06.4 Umbraco version CMS Verify running latest patch of Umbraco 17 — check release notes for security fixes Medium A06.5 Next.js version Frontend Verify running latest patch of Next.js 16 — check security advisories Medium A06.6 Outdated JS libraries Client bundle Analyze client-side JS for known vulnerable libraries (e.g., outdated jQuery, lodash) Medium
# Test Target Procedure Severity A07.1 MFA enforcement Backoffice Attempt login without MFA — must be blocked at Azure AD level Critical A07.2 Session fixation Auth Gate Check if __dev_gate cookie changes after successful login (new JWT issued) High A07.3 Cookie security flags Auth Gate Verify __dev_gate cookie has HttpOnly, Secure, SameSite=Lax High A07.4 API key in URL Stats endpoint Verify API key is passed via header (x-dashboard-key), not query string in production (query string appears in logs) Medium A07.5 Password policy Umbraco Verify ASP.NET Identity enforces minimum complexity (length, special chars) Medium A07.6 JWT expiration Auth Gate Verify JWT exp claim is validated — expired tokens rejected High A07.7 Logout invalidation Auth Gate After logout, verify old JWT cookie is cleared and cannot be reused Medium
# Test Target Procedure Severity A08.1 Webhook signature bypass Webhook Modify payload body after signing — verify HMAC check fails Critical A08.2 Subresource Integrity CDN scripts Verify third-party scripts (GTM, reCAPTCHA, Cookiebot) use SRI hashes or are loaded from trusted origins Medium A08.3 CI/CD pipeline integrity Azure DevOps Verify pipeline YAML is protected — only maintainers can modify .azure/pipelines/ High A08.4 Package integrity pnpm Verify pnpm-lock.yaml is committed and --frozen-lockfile used in CI Medium A08.5 Docker image provenance ACR Verify Docker images are pulled from trusted base (node:20-alpine official) Medium
# Test Target Procedure Severity A09.1 Failed login logging Auth Gate Trigger 5 failed logins — verify events appear in Application Insights High A09.2 Webhook rejection logging Webhook Send unsigned webhook — verify 401 is logged with source IP High A09.3 Azure activity log Key Vault Access Key Vault secret — verify audit log captures the access event Medium A09.4 WAF logging Cloudflare Trigger a WAF rule (e.g., SQLi attempt) — verify it appears in Cloudflare Security Events Medium A09.5 Alerting on anomalies All Verify alerts fire when error rate exceeds 1% (5xx) for 5 minutes Medium A09.6 No sensitive data in logs Application Insights Review logs — verify no passwords, API keys, or PII in log entries High
# Test Target Procedure Severity A10.1 SSRF via media proxy Next.js rewrites Manipulate /media/:path* rewrite — attempt to access internal Azure resources (169.254.169.254 metadata) Critical A10.2 SSRF via preview path /api/draft?path=Send path=http://169.254.169.254/metadata — must be rejected (path validation exists) High A10.3 SSRF via image loader Cloudflare image loader Manipulate image URL to point to internal resource — verify loader restricts to allowed domains High A10.4 SSRF via webhook URL Webhook handler If webhook triggers fetch to external URL, verify URL is validated against allowlist Medium
Beyond OWASP Top 10, these tests are specific to the Savoy headless architecture.
# Test Procedure Severity CF.1 WAF bypass Test known bypass techniques for Cloudflare Managed Ruleset — verify rules are current High CF.2 Bot detection Use automated tools (Selenium, Puppeteer) — verify Super Bot Fight Mode challenges or blocks Medium CF.3 DDoS resilience Verify DDoS protection is active (Cloudflare dashboard, not actual attack) High CF.4 Cache poisoning Inject Host, X-Forwarded-Host, X-Original-URL headers — verify cache key is not poisoned High CF.5 Origin IP exposure Search for origin IP in DNS history, Shodan, Censys — verify origin is not directly reachable High
# Test Procedure Severity MS.1 Cross-site content access As authenticated user of site A, attempt to read/modify content of site B via API Critical MS.2 Cross-site cookie scope Verify __dev_gate cookie domain scope doesn’t leak to unrelated subdomains Medium MS.3 Theme/asset isolation Verify one site’s assets cannot reference another site’s resources Low MS.4 Shared Content security Verify Shared Content node is read-only for hotel-scoped editors High
# Test Procedure Severity API.1 Pagination abuse Request ?skip=0&take=999999 — verify server enforces max page size Medium API.2 Filter injection Test filter parameters with OData operators ($filter, eq, ne, or) — verify sandboxed Medium API.3 Draft content leakage Without preview mode, request unpublished content — must return 404 High API.4 API key enumeration Brute-force API key — verify rate limiting or lockout Medium API.5 Response size limit Request deeply nested Block List content — verify response size is bounded Low
# Test Procedure Severity FORM.1 CSRF protection Submit form without CSRF token — must be rejected High FORM.2 File upload validation Upload .php, .exe, .html as form attachment — must be rejected or sanitized Critical FORM.3 File size limit Upload oversized file (100MB+) — must be rejected with clear error Medium FORM.4 GDPR consent required Submit form without consent checkbox — must be rejected High FORM.5 Email injection Inject CRLF in form email fields ([email protected] \r\nBcc:[email protected] ) — must be sanitized High FORM.6 Spam / bot protection Submit form without reCAPTCHA token — must be rejected Medium FORM.7 PII in response Submit form — verify response does NOT echo back submitted PII Medium
These are security gaps identified in the current codebase that must be resolved before production go-live.
# Gap Current State Required State Priority GAP.1 No CSP header Not configured in next.config.ts or middleware Strict CSP with nonces (remove unsafe-inline, unsafe-eval) Critical GAP.2 No security headers Missing X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy All headers configured in next.config.ts Critical GAP.3 No HSTS header Not set Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadHigh GAP.4 Rich Text rendered as raw HTML CMS content rendered with raw HTML insertion (trusted source) Acceptable for trusted CMS content — document as known risk, add CSP as mitigation Medium GAP.5 No rate limiting on API routes Only Cloudflare rate limits (if configured) Add application-level rate limiting on /api/gate/login (5 req/min), /api/webhooks (10 req/min) High GAP.6 Stats API key in query param ?key= accepted alongside headerRemove query param option — keys in URLs appear in server logs and browser history Medium GAP.7 No webhook replay protection Same payload can be replayed Add timestamp check — reject payloads older than 5 minutes Medium GAP.8 NODE_TLS_REJECT_UNAUTHORIZED=0Set in .env.local Ensure NOT set in any deployed environment (STAGE/QA/PROD) Critical GAP.9 Default credentials in dev config Admin1234! in appsettings.Development.jsonVerify these credentials are NOT used in any deployed environment Critical GAP.10 No API key rotation Static UMBRACO_API_KEY, REVALIDATE_SECRET Document rotation procedure, implement Key Vault rotation schedule Medium
Phase When Scope Who Per PR Every PR to develop Automated: pnpm audit, lint, typecheck CI pipeline (automatic) Per Deploy Every push to deploy/dev Automated: dependency scan, header check CI pipeline (automatic) Pre-Go-Live Before first production deploy Full pentest — ALL tests in this document Security team / external Quarterly Every 3 months after go-live Full OWASP scan + dependency audit Security team On Incident After any security incident Targeted re-test of affected area Security team On Major Release New Umbraco/Next.js major version Full pentest — focus on new attack surface Security team
Production deployment is blocked until ALL items pass.
Infrastructure:
Application:
Authentication & Access:
API Security:
Compliance:
Scanning:
Add these stages to .azure/pipelines/deploy-dev.yml:
Stage Tool Command Blocks Deploy? Dependency Audit pnpm audit pnpm audit --audit-level=highYes (high/critical) NuGet Audit dotnet dotnet list package --vulnerable --include-transitiveYes (high/critical) Docker Image Scan Trivy trivy image --severity HIGH,CRITICAL acrsavoydev.azurecr.io/savoy-nextjs:latestYes (critical) Bundle Analysis custom script Scan .next/static/ for leaked env vars (grep for UMBRACO_API_KEY, REVALIDATE_SECRET, etc.) Yes Header Verification curl + script Verify all security headers present on deployed URL Warning OWASP ZAP Baseline ZAP zap-baseline.py -t $DEPLOY_URL -r report.htmlWarning (report-only initially, blocking after hardening)
# Dependency audit (CI stage)
pnpm audit --audit-level=high
# NuGet vulnerability check (CI stage)
~ /.dotnet/dotnet list apps/cms/Savoy.Cms/Savoy.Cms.csproj package --vulnerable
# Docker image scan (after build)
trivy image --severity HIGH,CRITICAL --exit-code 1 acrsavoydev.azurecr.io/savoy-nextjs:latest
# Bundle secret scan (after build)
grep -r " UMBRACO_API_KEY\|REVALIDATE_SECRET\|CLOUDFLARE_API_TOKEN\|GATE_SECRET " \
apps/web/.next/static/ && echo " FAIL: SECRETS FOUND IN BUNDLE " && exit 1 || echo " PASS: Clean "
# Security headers check (after deploy)
HEADERS = $( curl -sI " $DEPLOY_URL " )
echo " $HEADERS " | grep -qi " x-frame-options " || { echo " MISSING: X-Frame-Options " ; MISSING = 1 ; }
echo " $HEADERS " | grep -qi " x-content-type-options " || { echo " MISSING: X-Content-Type-Options " ; MISSING = 1 ; }
echo " $HEADERS " | grep -qi " strict-transport-security " || { echo " MISSING: HSTS " ; MISSING = 1 ; }
echo " $HEADERS " | grep -qi " content-security-policy " || { echo " MISSING: CSP " ; MISSING = 1 ; }
echo " $HEADERS " | grep -qi " referrer-policy " || { echo " MISSING: Referrer-Policy " ; MISSING = 1 ; }
[ $MISSING -eq 0 ] && echo " PASS: All security headers present "
# OWASP ZAP baseline (after deploy)
docker run --rm -v $( pwd ) :/zap/wrk owasp/zap2docker-stable zap-baseline.py \
-t " $DEPLOY_URL " -r zap-report.html -l WARN
Tool Purpose Installation Frequency OWASP ZAP Automated web scanner + proxy brew install zaproxyPer deploy + quarterly Burp Suite Community Manual interception and testing Download from PortSwigger Pre-go-live + quarterly nuclei Template-based vulnerability scanner brew install nucleiWeekly (automated) nikto Web server scanner brew install niktoPre-go-live + quarterly testssl.sh TLS/SSL configuration audit brew install testsslPre-go-live + monthly trivy Container image CVE scanner brew install trivyPer Docker build (CI) SecurityHeaders.com HTTP header audit Online tool Per deploy SSL Labs SSL certificate + config grading Online tool Monthly Shodan / Censys Origin IP discovery check Online tool Pre-go-live + quarterly
For pre-go-live and quarterly pentests, follow this workflow:
|-- DNS enumeration (subdomains, MX, TXT records)
|-- Port scanning (Nmap — verify only 443 exposed via Cloudflare)
|-- Technology fingerprinting (Wappalyzer)
+-- Origin IP discovery (Shodan, Censys, DNS history)
|-- OWASP ZAP full scan (authenticated + unauthenticated)
|-- nuclei scan (community templates)
3. Manual Testing (per OWASP section above)
|-- A01 — Access control tests (cross-site, RBAC, auth bypass)
|-- A02 — Crypto tests (TLS, JWT, secrets)
|-- A03 — Injection tests (XSS, SQLi, SSRF)
|-- A04 — Design flaws (rate limiting, enumeration)
|-- A05 — Misconfiguration (headers, debug, defaults)
+-- A06-A10 — Remaining OWASP categories
|-- Document all findings with severity (Critical/High/Medium/Low)
|-- Provide reproduction steps for each finding
|-- Prioritize remediation timeline
# -- Reconnaissance -------------------------------------------------------
nslookup -type=ANY savoysignature.com
dig +short savoysignature.com
nmap -sV -T4 savoysignature.com
# -- TLS Audit ------------------------------------------------------------
testssl --severity HIGH https://savoysignature.com
# -- OWASP ZAP Scans ------------------------------------------------------
# Baseline (passive — safe)
zap-baseline.py -t https://savoy.dev-signature.wycreative.com -r baseline.html
# Full scan (active — runs attacks, use on DEV only)
zap-full-scan.py -t https://savoy.dev-signature.wycreative.com -r full-scan.html
# API scan (Delivery API)
zap-api-scan.py -t https://savoy.dev-cms.wycreative.com/umbraco/delivery/api/v2 \
-f openapi -r api-scan.html
# -- nuclei ----------------------------------------------------------------
nuclei -u https://savoy.dev-signature.wycreative.com -severity high,critical -o nuclei-report.txt
# -- nikto -----------------------------------------------------------------
nikto -h https://savoy.dev-cms.wycreative.com -o nikto-report.html -Format htm
# -- Docker Image Scan -----------------------------------------------------
trivy image --severity HIGH,CRITICAL acrsavoydev.azurecr.io/savoy-nextjs:latest
# -- Dependency Audit ------------------------------------------------------
pnpm audit --audit-level=high
~ /.dotnet/dotnet list apps/cms/Savoy.Cms/Savoy.Cms.csproj package --vulnerable
# -- Security Headers ------------------------------------------------------
curl -sI https://savoy.dev-signature.wycreative.com \
| grep -iE " x-frame|csp|hsts|x-content|referrer|permissions "
# -- Bundle Secret Scan ----------------------------------------------------
grep -rn " UMBRACO_API_KEY\|REVALIDATE_SECRET\|CLOUDFLARE_API_TOKEN\|GATE_SECRET " \
# -- SSL Labs --------------------------------------------------------------
# Use https://www.ssllabs.com/ssltest/ — target: A+ grade
Severity Description SLA (Fix Time) Blocks Go-Live? Critical Remote code execution, auth bypass, data breach, secret exposure 24 hours Yes High XSS, CSRF, IDOR, missing security headers, weak crypto 72 hours Yes Medium Information disclosure, rate limiting gaps, session issues 2 weeks No (but must fix before next quarterly audit) Low Minor info leak, verbose errors, missing best practices Next sprint No
Each pentest report must follow this structure:
# Security Pentest Report — Savoy Signature
** Tester: ** [Name / Company]
** Scope: ** [DEV / STAGE / PROD] — [URLs tested]
** Tools Used: ** [ZAP, Burp, nuclei, etc.]
- Total findings: X (Y Critical, Z High, W Medium, V Low)
- Go-live recommendation: PASS / FAIL
### [ SEVERITY ] Finding Title
- ** OWASP Category: ** A0X
- ** Test ID: ** A0X.Y (from this document)
- ** Target: ** [URL / endpoint]
- ** Description: ** [What was found]
- ** Reproduction Steps: ** [ Step-by-step ]
- ** Evidence: ** [Screenshot / curl command / response]
- ** Impact: ** [What an attacker could do]
- ** Remediation: ** [How to fix]
- ** Status: ** Open / In Progress / Fixed / Accepted Risk
| # | Finding | Severity | Status | Fix Date |
|---|---------|----------|--------|----------|
Cloudflare’s security features will interfere with penetration testing. Follow this procedure:
Whitelist tester IP in Cloudflare WAF > Tools > IP Access Rules > Allow
Disable Bot Fight Mode temporarily (or whitelist scanner UA)
Disable rate limiting for tester IP (or increase thresholds)
Note: Some tests must target the origin directly (*.azurewebsites.net) to bypass Cloudflare — document which
Test Scope Target URL Cloudflare State WAF bypass tests Production Cloudflare domain WAF ON (testing the WAF itself) Application-level tests Direct Azure URL or whitelisted IP WAF bypassed (testing the app) TLS tests Production Cloudflare domain Full proxy ON Origin exposure tests Shodan / Censys / DNS history N/A
Remove IP whitelist from WAF rules
Re-enable Bot Fight Mode
Restore rate limiting rules
Verify WAF event log shows expected blocks from test traffic
Next document: Return to 15_Security_and_Data_Protection.md for architecture-level security controls.