Code and Security Review

Reviewed against main at commit cd5ae9d on June 14, 2026.


Resolution log (June 14, 2026)

Fixed in code (verified via wrangler dev + npm run check):

Needs a Cloudflare dashboard / account action (not code):

Decisions (resolved):

Done — Cloudflare dashboard / account (2026-06-14):

Still open — Cloudflare dashboard / account (not code):

Still open — larger / ongoing (partially addressed or beyond current scope):


Code Review Findings

P1: Renaming can overwrite another booking

worker.js:162

Saving a renamed booking writes the new reference without checking whether it already exists, then deletes the original. Accidentally renaming FH-A to FH-B permanently replaces FH-B without creating a trash backup. Reject reference collisions unless explicitly confirmed.

P1: Empty cruise-port fields shift columns

js/admin.js:45

rowsToText removes empty fields before joining them. Because parseRows later restores fields by position, an empty arrival time causes the departure time and every following value to shift into the wrong fields after saving and reopening.

P2: Admin list silently truncates

worker.js:160

Unlike snapshotAll, this KV list call does not follow pagination cursors. Once the namespace exceeds one page, bookings disappear from the admin list even though they remain stored and accessible directly.

P2: Failed deletes are reported as successful

js/admin.js:80

The delete handler ignores response status and always clears the form and reports success. An expired admin session or Worker failure can leave the booking intact while telling the administrator it was deleted.

Security Review Findings

P1: The public lookup can bypass the intended hostname and zone rate limit

worker.js:26, wrangler.jsonc

The Worker accepts /api/lookup before applying any hostname restriction. wrangler.jsonc does not set workers_dev = false and does not declare routes; Wrangler therefore enables the workers.dev endpoint by default. Requests to that endpoint can reach the lookup API outside a WAF rate-limiting rule configured for the farholm.com zone.

This matters because a successful lookup exposes itinerary details including traveler names, confirmation numbers, ticket numbers, loyalty numbers, dates, and locations. Restrict lookup to trip.farholm.com, disable workers.dev, and enforce throttling in the Worker or with a Rate Limit binding.

P1: Admin authentication fails open when Access verification config is absent

worker.js:104-118

When either ACCESS_TEAM_DOMAIN or ACCESS_AUD is missing, accessEmail accepts the plaintext cf-access-authenticated-user-email header or decodes an unsigned JWT payload. A deployment/configuration regression therefore removes the Worker's independent authentication check and makes authorization depend entirely on an external Access policy being perfect.

Production should fail closed unless a verified Access JWT is present. Keep the existing explicit localhost bypass as the only development exception.

P2: Booking access codes use a short, non-cryptographic generator

js/admin.js:74

Generated references contain five characters selected with Math.random, providing only about 25 bits of reference entropy. The second lookup factor is a surname, which is commonly known or guessable. This is weak protection for the sensitive itinerary data returned by the public lookup route, especially if rate limiting is bypassed or misconfigured.

Generate substantially longer references with crypto.getRandomValues, or use expiring signed links or a stronger client authentication flow.

P2: Public lookup input is not bounded or validated

worker.js:131, worker.js:156

The public endpoint accepts arbitrary-length and arbitrary-format references and surnames. Oversized references can exceed KV key limits and turn a normal lookup miss into an unhandled Worker error; repeated malformed requests can consume Worker resources and produce noisy failures.

Enforce a strict reference format and small request-field limits before accessing KV, and return a controlled 400 or generic 404.

Security Hardening Notes

Security Checks Run

Privacy and Data-Protection Review

P1: There is no complete client-data erasure path

worker.js:135-150, worker.js:162, worker.js:165

Live bookings have no expiration or retention metadata. Deleting a booking copies it to trash for 90 days, and daily snapshots retain additional copies for up to 90 days. There is no operation that purges a client's data from the live record, trash, and snapshots together. Define a retention policy and add a documented, auditable erasure workflow.

P2: The public API returns data the client page does not need

worker.js:156, worker.js:173

bookingForClient returns a clone of the entire booking, including lastName. The lookup surname is used only for authentication and is never rendered by the client page. Return an explicit allowlist of client-visible fields instead of a nearly complete stored record.

P2: Privacy and cookie disclosures cannot be verified from the repository

privacy.njk:20-28, cookies.njk:20-28

Both policies are rendered entirely by Termly at runtime. Without JavaScript, users receive only an email address, and reviewers cannot confirm that the published policy covers itinerary KV storage, R2 backups, retention periods, Google Analytics, Web3Forms, or client erasure requests. Keep a reviewable policy copy or automated disclosure checklist in the repository.

Reliability and Recovery Review

P1: Same-day snapshots overwrite previous recovery points

worker.js:141-144

All automatic and manual snapshots use snapshots/YYYY-MM-DD.json. A manual snapshot after accidental corruption can overwrite the healthy snapshot from earlier that day, and the scheduled snapshot can overwrite a manual recovery point. Include a timestamp or unique identifier in every snapshot key.

P2: Restore capability is manual and untested

README.md:350-364, scripts/check.mjs:89-127

The application creates and prunes snapshots, but it has no restore command, admin action, validation step, or restore test. The documented process requires manually downloading and re-saving records during an incident. Add a dry-run-capable restore tool and regularly test recovery into a separate namespace.

P2: The health endpoint does not test critical dependencies

worker.js:25

/api/health always returns success without checking KV, R2, or Access configuration. Monitoring can report a healthy portal while lookup, admin, or backups are broken. Add dependency-aware readiness checks or separate monitors.

P2: Concurrent admin edits silently overwrite each other

worker.js:162

Bookings contain no version or update timestamp, and saves perform unconditional KV writes. Two admin tabs can edit the same booking and the last save silently destroys the other changes. Add optimistic concurrency with a version token.

Functional Workflow Review

P2: Lookup outages are shown as “booking not found”

js/trip.js:236-238

The client UI maps every non-success response and network failure to the same not-found message. A Worker, KV, or network outage tells clients to recheck their details rather than reporting a temporary service problem. Distinguish 404 from service and network failures.

P2: Calendar exports have unstable identities and incomplete ICS handling

js/trip.js:189-207

Event UIDs are based on array position, so reordering itinerary items changes their identity and can create duplicate calendar events. DTSTAMP is also hard-coded, and long content lines are not folded as required by ICS clients. Use stable segment IDs, a current timestamp, and standards-compliant line folding.

Verified Workflows

Accessibility and Responsive Review

P2: Hidden contact-form radio inputs cause horizontal scrolling

css/styles.css:591-600, css/styles.css:868-869

The general form rule gives every input width: 100%. The pill-group radios are then positioned absolutely but retain that width, extending past their container. Browser checks measured the contact page at 423px wide in a 375px viewport and 1444px wide in a 1280px viewport. Use a standard visually-hidden control pattern with fixed dimensions or override the radio width.

Accessibility Checks

Web, Tablet, and Phone Display Review

P2: Navigation is cramped immediately above the mobile breakpoint

css/styles.css, _includes/nav.njk

At 1025px wide, the site switches from the hamburger menu to the full desktop navigation before there is enough room for it. The logo touches the Home link, and Points & Miles, Why Us, and View My Booking wrap onto two lines. At 1100px the navigation displays cleanly. Raise the hamburger breakpoint or reduce desktop navigation spacing.

P2: The consent banner dominates small screens and overlaps the open menu

_includes/base.njk:57

The Termly banner occupies about 303px of an 844px-tall phone viewport and 345px of a 700px-tall small-phone viewport. When the mobile menu is open, the banner partially covers its Start Planning CTA. Review Termly's compact mobile configuration or ensure the menu and consent layer do not overlap.

Responsive Display Checks

Performance and SEO Review

P2: A third-party consent script blocks parsing on every marketing page

_includes/base.njk:57

The Termly resource-blocker script is synchronous in the document head. A slow or unavailable third party can delay parsing and rendering across the public site. Confirm whether its blocking mode is required and measure its effect on Core Web Vitals; otherwise load it with a less disruptive integration.

Performance and SEO Checks

Operational Configuration Review

P1: Critical production controls are not represented or tested as code

wrangler.jsonc, README.md:408-427

Cloudflare Access applications, WAF rate limits, custom domains, redirects, and health alerts are described as dashboard setup steps but are not defined or verified in CI. Configuration drift can silently remove authentication, throttling, routing, or monitoring. Manage these controls as infrastructure as code where possible and add deployment smoke tests for the live hostnames.

Operational Checks

Live-Production Smoke Review

Reviewed live production on June 14, 2026 without submitting forms or changing booking data.

P1: HTTP is served without redirecting to HTTPS

Live: http://farholm.com, http://trip.farholm.com, http://cruise.farholm.com

All three hosts returned 200 OK over plain HTTP instead of redirecting to HTTPS. HTTPS responses also did not include Strict-Transport-Security. Visitors can therefore receive pages over an unauthenticated, modifiable connection. Enable Cloudflare Always Use HTTPS and HSTS after confirming all required hosts support HTTPS.

P1: Live Privacy and Cookie Policy pages render empty

Live: https://farholm.com/privacy, https://farholm.com/cookies

The Termly policy embed throws a CSP error because it evaluates JavaScript strings while the site's script-src does not allow unsafe-eval. Both policy containers remained empty in a live browser. Resolve this without broadly weakening CSP if possible, or publish reviewable first-party policy content.

P2: Sitemap and canonical URLs point to redirecting addresses

Live: https://farholm.com/sitemap.xml

Every .html sitemap URL except the homepage returned a 307 redirect to an extensionless URL. Page canonical metadata also uses the redirecting .html addresses. Update internal links, canonicals, and sitemap output to the final extensionless URLs, or stop redirecting the .html versions.

P2: Dynamic public routes are active on every custom hostname

Live: /api/lookup, /api/health

The lookup and health APIs respond on farholm.com, trip.farholm.com, and cruise.farholm.com, rather than only on their intended portal hostname. Production rate limiting did apply across the tested custom hosts, but the unnecessarily broad route exposure increases configuration complexity and attack surface.

Live Checks That Passed

Live Failure Reproduced

Dependency and Supply-Chain Review

Reviewed the committed dependency tree, install behavior, CI workflow, and third-party build inputs on June 14, 2026.

P2: The current development toolchain contains a high-severity advisory

npm audit reports a high-severity advisory in esbuild@0.27.3, pulled in by wrangler@4.100.0. The advisory concerns missing binary integrity verification when an attacker controls NPM_CONFIG_REGISTRY; a separate low-severity advisory affects the development server on Windows.

The repository is already using the latest Wrangler release, and npm currently reports no fix as available. This does not affect the deployed site's runtime: npm audit --omit=dev reports zero vulnerabilities. Continue using the official npm registry in trusted build environments, avoid exposing the local development server, and update Wrangler as soon as it moves to esbuild@0.28.1 or later.

P2: CI actions are mutable and workflow permissions are inherited

.github/workflows/ci.yml:16-17 references actions/checkout@v4 and actions/setup-node@v4 using moving tags. If either tag or its publishing account were compromised, CI would execute changed code on the next run. The workflow also does not declare a permissions block, so its token permissions depend on repository-level defaults.

Pin both actions to reviewed full commit SHAs and declare the minimum required workflow permissions, which for this read-only build should be contents: read.

P2: Vulnerable or stale dependencies are not detected automatically

The repository has no Dependabot configuration, and .github/workflows/ci.yml:21-25 installs, builds, and runs project checks without an audit or dependency-review step. The current high-severity Wrangler advisory can therefore remain unnoticed until someone manually runs an audit.

Enable Dependabot or Renovate for npm and GitHub Actions. Add a scheduled audit or GitHub dependency-review check with an explicit policy for development-only advisories that have no available fix, so an unavoidable warning does not silently normalize future actionable vulnerabilities.

Supply-Chain Checks

Content Quality and Trust Review

Reviewed client-facing copy, contact destinations, internal links, metadata, and trust claims on June 14, 2026.

P2: Strong credential and protection claims need reviewable support

about.njk:87 and contact.njk:158 say that IATA and CLIA credentials prove strict standards for financial accountability and client protection, while the About page also states that Farholm carries professional liability insurance. The repository contains badge images and membership numbers, but no review date, evidence link, or process ensuring these claims remain current and use credential-provider-approved wording.

Confirm the wording against current IATAN/CLIA rules and the active insurance policy. Prefer narrower factual wording, link to independently verifiable credentials where possible, and schedule an annual review before publishing a new dated CLIA badge.

P3: Client-service promises are broader than their stated limits

The site promises a response “within 24 hours,” says alternative contact options “always work,” invites visitors to “reach out anytime,” and describes booking transfers as having “no downside.” These phrases create expectations that may not hold during weekends, outages, supplier restrictions, or after a transfer changes who controls a booking.

Use specific, sustainable commitments such as “within two business days,” and qualify transfer and support claims with supplier eligibility and availability.

Content and Link Checks

Analytics, Monitoring, and Incident-Response Review

Reviewed repository-defined analytics, health checks, Worker diagnostics, scheduled jobs, and operational documentation on June 14, 2026. Dashboard-only Cloudflare alerts and analytics settings could not be verified from the repository.

P1: Scheduled backup failures are silent

worker.js:19-20, worker.js:135-150

The daily scheduled handler passes snapshotAll(env) to ctx.waitUntil() but does not record success or failure, and there is no alert when KV reads, the R2 write, or pruning fails. A broken backup job can therefore remain unnoticed until a restore is needed.

Emit a privacy-safe structured result for every scheduled run and alert when a run fails, writes zero bookings unexpectedly, or no fresh snapshot exists within the expected window. Monitor the backup artifact itself rather than only whether the cron was invoked.

P2: Worker failures have little diagnostic evidence

worker.js:13-16, worker.js:94-100, worker.js:156-164, wrangler.jsonc

The Worker has no structured application logging, error correlation, or repository-defined observability configuration. Several dependency, parsing, and storage errors are intentionally caught and discarded, making Access, exchange-rate, corrupt-record, and external-service failures difficult to distinguish after an incident.

Enable privacy-safe Worker logs and traces with an explicit sampling and retention policy. Log route, outcome, status, duration, dependency, and a request or Cloudflare Ray identifier, but never surnames, booking references, tokens, itinerary contents, or third-party API keys.

P2: There is no incident-response runbook

No runbook or playbook defines incident ownership, severity levels, escalation contacts, service priorities, communication templates, recovery objectives, or the steps for security and privacy incidents. The README documents individual backup and health-check mechanics, but not how to coordinate a real outage or data event.

Create a short operational runbook covering portal outage, failed backups, unauthorized access, lost or corrupted booking data, contact-form failure, and third-party outages. Include decision owners, evidence-preservation steps, client-notification criteria, rollback and restore procedures, and a periodic exercise schedule.

P3: Analytics do not measure core business journeys

_includes/base.njk:43-65, js/site.js:78-111, _data/site.json:14

Google Analytics is configured for pageviews, but the site emits no explicit events for successful or failed inquiry submissions, contact-channel clicks, cruise referral clicks, or progression to the booking portal. Cloudflare Web Analytics is not enabled through the committed token, though dashboard-side automatic injection may still be active.

Define a small consent-aware measurement plan for inquiry success and failure, contact clicks, and cruise referrals. Avoid sending names, email addresses, booking references, surnames, free-text messages, or itinerary details to analytics providers.

Monitoring and Response Checks

Test Coverage and Release-Readiness Review

Reviewed automated checks, CI triggers, production deployment flow, release documentation, and rollback evidence on June 14, 2026. GitHub branch-protection and Cloudflare Workers Builds dashboard settings could not be verified from the repository.

P1: Production deployment is not demonstrably gated by CI

README.md:9-22, .github/workflows/ci.yml:6-25

Every push to main triggers a Cloudflare production deployment, while GitHub CI independently runs on the same push. Nothing in the repository makes the deployment wait for CI success, and the documented Cloudflare build command does not include npm run check. A syntactically valid site with failing security or behavior checks can therefore begin deploying before GitHub CI reports failure.

Deploy only after required checks pass, or make the Cloudflare production build run the same complete verification command before deployment. Protect main from direct pushes and require the build-and-check job on pull requests.

P2: Production-critical workflows have almost no automated coverage

scripts/check.mjs, worker.js:156-167, js/admin.js:72-82, js/trip.js:180-239, js/site.js:78-111

Automated behavior checks cover Access-token verification and the happy path of snapshot creation and pruning. They do not cover public lookup, admin authorization routing, booking create/edit/rename/delete behavior, pagination, client-data filtering, dependency failures, contact submission, itinerary rendering, calendar export, or browser interactions. Several defects already identified in this review would have been caught by focused tests.

Add Worker integration tests with fake KV/R2 bindings and a small browser suite covering the contact form, demo lookup, itinerary rendering, and admin CRUD. Prioritize regression tests for every accepted code-review finding.

P2: Current behavior tests execute copied function text, not the Worker

scripts/check.mjs:48-58, scripts/check.mjs:93-101

The checks extract functions from worker.js using regular expressions and execute them through new Function. This can break when functions are reformatted or begin depending on module-level helpers, and it does not prove that the deployed Worker module, routing, bindings, or response headers behave correctly together.

Make Worker logic importable and test it through the same module boundary used in production, ideally using Cloudflare's Worker testing support. Keep a small end-to-end smoke test for deployed-host behavior.

P2: Rollback and release verification are undocumented and unrehearsed

The repository has no staging environment, release checklist, rollback procedure, deployment marker, or automated post-deploy smoke workflow. Production smoke testing was performed manually during this review, but there is no repeatable process ensuring each deployment serves the expected pages, protects admin routes, and preserves lookup behavior.

Document how to identify and roll back to a known-good Worker version, then rehearse it. Add a post-deploy read-only smoke check for the main, trip, and cruise hosts, including HTTPS behavior, key static pages, health/readiness, generic invalid lookup, and protected admin routes.

Test and Release Checks

Data Lifecycle and Records-Management Review

Reviewed data collection, storage copies, retention behavior, deletion paths, third-party transfers, and access governance on June 14, 2026. This review builds on the incomplete-erasure finding in the earlier Privacy and Data-Protection Review.

P1: Live booking records have no retention limit

worker.js:162, wrangler.jsonc:15-24

Live booking: records are stored in KV without expiration metadata and remain until manually deleted. The system does not record trip completion, record creation, last update, retention category, or a disposal date, so there is no reliable way to identify records that should be archived or deleted.

Define retention periods based on business and legal needs, add lifecycle metadata to every booking, and automate a review or purge queue. Separate active-trip operational records from records retained for accounting, dispute, or legal purposes.

P2: Sensitive itinerary fields are collected without classification rules

js/itinerary-schema.js, worker.js:162, worker.js:169-174

Booking records can contain traveler and guest names, loyalty numbers, ticket numbers, confirmation codes, travel dates, locations, costs, contact details, and unrestricted notes. These fields are copied into KV, soft-delete records, and full R2 snapshots, but there is no documented classification, prohibited data list, or field-specific retention rule.

Create a data-classification and minimization standard for the itinerary editor. Explicitly prohibit passports, payment-card data, account passwords, medical details, and other unnecessary sensitive information; label fields that require shorter retention or redaction.

P2: Inquiry retention is controlled outside the application

contact.njk:26-117, js/site.js:78-111

Contact-form submissions send names, contact details, state, budget, travel timing, party type, and free-text messages directly to Web3Forms. The repository defines no retention period, deletion workflow, export procedure, or reconciliation process for those submissions and any resulting email copies.

Document where inquiry copies are stored after submission and configure retention in Web3Forms and email systems. Include those systems in access, export, correction, and erasure procedures.

P2: There is no maintained data inventory or access-review record

The README describes individual systems, but there is no single inventory mapping data categories to purpose, owner, storage location, recipients, retention period, deletion mechanism, and authorized roles. There is also no documented periodic review of Cloudflare account access, Access policy members, Web3Forms, Termly, Google Analytics, email, or API-provider accounts.

Maintain a lightweight records-of-processing/data inventory and review access at a defined interval and after personnel or vendor changes. Record completed reviews and removal of obsolete accounts or credentials.

Data Lifecycle Checks

Business-Continuity and Vendor-Risk Review

Reviewed critical providers, service dependencies, fallback behavior, account recovery evidence, and vendor continuity documentation on June 14, 2026. Provider contracts, account security settings, billing contacts, and recovery methods could not be verified from the repository.

P1: Primary data and backups share one provider and account boundary

wrangler.jsonc:15-28, worker.js:133-150

Cloudflare provides the live Worker, static assets, DNS/routing controls, Access authentication, primary KV booking records, scheduled jobs, and R2 backups. R2 protects against loss of the KV namespace, but it does not protect against Cloudflare account loss, account compromise, billing suspension, provider-wide failure, or an erroneous deletion affecting both stores.

Maintain a periodically verified encrypted export of booking data outside the Cloudflare account and document how to rebuild the essential client itinerary service elsewhere. Protect Cloudflare account recovery materials and use independent break-glass ownership with strong multifactor authentication.

P2: Legal disclosures depend entirely on Termly availability and compatibility

privacy.njk:19-28, cookies.njk:19-28, _includes/base.njk:45-57

Privacy and cookie disclosures are loaded entirely from Termly at runtime. When Termly is unavailable, blocked, or incompatible with the site's security policy, the pages contain no substantive policy text. This failure is already present in the live-production review.

Keep a current first-party copy or static fallback of each published policy and define how policy changes are reviewed and synchronized. Legal disclosures should remain available during a Termly outage or account loss.

P2: Critical vendor ownership and recovery information is undocumented

The repository identifies Cloudflare, GitHub, Web3Forms, Termly, Google, RapidAPI/AeroDataBox, Frankfurter, and Virgin Voyages integrations, but it does not document business owners, account owners, billing contacts, renewal dates, recovery methods, service criticality, data handled, contractual commitments, or exit procedures.

Create a vendor register and keep recovery codes, account-transfer procedures, and emergency contacts in an appropriately protected system. Review it periodically and whenever a provider, credential, or responsible person changes.

P2: There is no tested degraded-service plan for client-facing failures

js/site.js:93-110, worker.js:25-35, worker.js:156-164

Web3Forms failure falls back to an email link, and external place, flight, and exchange-rate services generally fail without corrupting records. However, there is no documented plan for operating during a booking-portal outage, Cloudflare Access failure, email outage, Web3Forms failure, or loss of a critical vendor account.

Define minimum service during each outage: how clients receive itineraries, where new inquiries are captured, how urgent-trip support is handled, and when the site should display a service notice. Exercise the highest-impact scenarios using non-production data.

Vendor-Continuity Checks

Abuse and Threat-Model Review

Reviewed likely attacker goals, abuse paths, privilege boundaries, destructive actions, injection defenses, spam controls, and the ability to detect and investigate abuse.

P1: A compromised admin identity has unrestricted destructive reach

The admin API can list, read, create, overwrite, and delete every booking, trigger snapshots, and use external lookup APIs (worker.js:160-167). There are no scoped roles, independent approval for high-impact actions, security event audit trail, or alerts for unusual admin activity. Soft deletion and snapshots help with mistakes, but provide limited protection if the admin identity or the shared Cloudflare account is maliciously controlled.

Action: Enforce strong MFA and tightly controlled recovery for Cloudflare Access and the Cloudflare account. Add an immutable audit trail for admin reads and changes, alerts for bulk or unusual activity, reauthentication or confirmation for destructive actions, and separate least-privilege roles where practical.

P2: Public lookup remains the highest-likelihood data-theft path

Successful public lookup reveals sensitive itinerary and identity-related details. The route is exposed before the admin-host restriction (worker.js:26, worker.js:156), while references are short and generated with Math.random (js/admin.js:74). The documented WAF rate limit helps on the intended hostname, but alternate Worker/custom-host access can bypass that perimeter control. This consolidates the related security-review findings into the primary external attacker scenario.

Action: Restrict lookup to the intended hostname inside the Worker, enforce rate limiting in application code or across every reachable hostname, replace references with longer cryptographically random values, and alert on repeated failed or distributed lookup attempts.

P2: The contact form can be abused for inbox flooding and phishing

The public contact form submits directly to Web3Forms (contact.njk:26-31, js/site.js:78-111). Its page-level honeypot can be bypassed by calling the provider directly with the intentionally public access key. This creates a practical path for spam, inbox flooding, phishing content, and operational distraction.

Action: Configure Web3Forms domain restrictions, provider-side spam controls, and rate limits. Add Turnstile or equivalent challenge controls if abuse occurs, and monitor/filter the receiving mailbox for bursts and malicious content.

P2: Abuse cannot be reliably detected or investigated

There is no security-focused audit trail or anomaly detection for repeated lookup failures, admin record access and changes, deletions, snapshot activity, or external API quota use. This makes it difficult to identify an attack early or determine its scope afterward.

Action: Record privacy-conscious security events with timestamps, actor identifiers, action types, outcomes, and request correlation IDs. Alert on suspicious lookup patterns, destructive admin activity, authentication anomalies, and unexpected API-volume increases. Define retention and access controls for these logs.

Threat Controls Observed