Tips

  • 🔐 Found a vulnerability? Report it via Contact Us — it pre-selects the Security Issue type so it reaches us fast.
  • This page is long — use the floating On this page button to jump straight to a section.
  • Each card covers a different layer: site-wide protections, the login page, your notes, and trip sharing.
  • Short version: data is encrypted in transit, logins and the contact form are rate-limited, and we never sell your data.

Site-Wide Security

Data Protection

  • All database queries use parameterized statements, which prevents SQL injection attacks.
  • User-provided content is HTML-encoded before being displayed, preventing cross-site scripting (XSS) attacks.
  • Server-side validation is performed on all inputs independently of any client-side validation.
  • Field length limits are enforced server-side against actual database column sizes.

Authentication & Session Management

  • Authentication uses an encrypted and cryptographically signed Forms Authentication ticket.
  • If your session data is lost (for example, after a server restart), it is automatically rebuilt from your valid authentication ticket — you remain logged in without re-entering your credentials.
  • Sessions use non-default cookie names to reduce automated targeting.
  • The authentication ticket is decrypted inside an error-handling block — malformed or tampered cookies are silently discarded rather than causing an application error.
  • Session data is loaded from the database once per request and held in memory for that request’s lifetime, reducing round-trips and ensuring all code within a single request sees consistent values.

Access Control

  • Authorization is verified independently on every sensitive operation, not just when a page first loads.
  • Authentication state is re-evaluated on every page load and every AJAX call. There is no endpoint that trusts a check performed during a prior request.
  • Trip ownership and sharing permissions are checked on every read, write, and delete operation.

Security Headers

The following security headers are sent with every response:

  • X-Content-Type-Options: nosniff — prevents browsers from guessing content types, stopping MIME-sniffing attacks that could cause a non-HTML response to be interpreted and executed as a script.
  • X-Frame-Options: SAMEORIGIN — prevents the site from being embedded in iframes hosted on other origins, protecting against clickjacking and UI-redress attacks.
  • Referrer-Policy: strict-origin-when-cross-origin — sends only the origin (not the full URL) to third-party sites when you follow a link, preventing path and query-string data from leaking to external destinations.
  • Permissions-Policy — denies a broad set of powerful browser APIs the application never uses: geolocation, microphone, camera, payment, usb, and the motion sensors (magnetometer, gyroscope, accelerometer). Even a fully compromised script running on the page cannot prompt for or silently access any of these capabilities. The same header additionally sets interest-cohort=() and browsing-topics=(), opting the site out of Google’s FLoC / Topics advertising-cohort APIs so visitors are never enrolled in interest-based ad profiling as a side effect of using the site.
  • Cross-Origin-Opener-Policy: same-origin — severs the window.opener reference between this site and any page on a different origin that opens it (or that it opens), closing the cross-window scripting and tab-nabbing channel that an attacker-controlled popup would otherwise use to reach back into the application’s browsing context.
  • Cross-Origin-Resource-Policy: same-origin — instructs the browser to refuse to embed Packlist PRO responses as a subresource on any other origin, blocking cross-origin resource inclusion and the speculative side-channel reads (Spectre-class) that such inclusion enables.
  • Strict-Transport-Security (HSTS): max-age=31536000; includeSubDomains — instructs browsers to use HTTPS exclusively for one full year, covering the entire packlistpro.net domain and every subdomain. Once received, the browser refuses plain-HTTP downgrades even if a user attempts to type an http:// URL.
  • The HSTS header is applied conditionally — only on responses served over a verified HTTPS connection, and never on localhost requests. This prevents a developer’s local environment from being permanently locked to HTTPS-only.
  • Content-Security-Policy (CSP) — restricts where browser content can be loaded from. default-src 'self' establishes the site’s own origin as the baseline for every content type; connect-src limits AJAX/fetch/WebSocket targets to the site itself plus the push-notification provider’s endpoints (no other cross-origin network destination is reachable); frame-ancestors 'self' reinforces the X-Frame-Options block at the modern-CSP layer; form-action 'self' prevents any form on the site from being repointed to submit user data to an external domain; base-uri 'self' prevents a <base> tag from being injected to relocate every relative URL; and object-src 'none' blocks embedding plugin objects (Flash, Java applets, generic <object>/<embed> tags) altogether.
  • The X-Powered-By, X-AspNet-Version, and IIS Server response headers are removed from every response (the Server banner is stripped both by requestFiltering removeServerHeader="true" and an explicit code-level removal), preventing automated scanners from identifying the server technology stack from the response banner.
  • Security headers are emitted by two independent mechanisms: the IIS <httpProtocol> block in Web.config (for production IIS) and the Application_BeginRequest handler in Global.asax (for IIS Express and other development environments). Both layers must fail simultaneously for a header to be missing from a response.

ViewState & Form Integrity

  • All ViewState is cryptographically signed (Message Authentication Code) and encrypted (viewStateEncryptionMode="Auto"). The encrypted, MAC-protected ViewState cannot be read by an attacker and cannot be modified without invalidating the signature.
  • EventValidation is enabled site-wide. The framework records the set of legitimate postback target IDs and event values when a page is rendered, and rejects any postback containing an event from a source not present in that original set. Attempts to fabricate postback events for hidden or disabled controls are rejected at the framework level before any application code runs.
  • ViewState is bound to your current session via ViewStateUserKey, which is set in the OnInit stage of every page — the earliest valid point in the page lifecycle. ViewState submitted in one user’s session cannot be replayed by another user, even if the raw bytes are captured.
  • ViewState encryption is applied to every page site-wide — it is not opt-in per page.

Input Filtering

  • ASP.NET request validation (validateRequest="true") rejects any form submission containing potentially dangerous HTML or script characters before it reaches application code.
  • ASP.NET response-header checking is enabled (enableHeaderChecking="true" in <httpRuntime>). The framework encodes carriage-return and line-feed characters in any value written to a response header, neutralising HTTP response-splitting and header-injection attempts in which attacker-controlled data — a redirect target, a cookie value — would otherwise inject a second header or a forged response body.
  • Incoming request body size is capped at approximately 26 MB server-side by two independent layers operating in agreement: the ASP.NET maxRequestLength setting in <httpRuntime> and the IIS-level requestLimits.maxAllowedContentLength setting in <system.webServer>. The lower of the two values controls. Both must be misconfigured before an oversized request can reach application code. Requests that exceed the limit are rejected by the framework before any application code runs, preventing oversized-payload denial-of-service attacks.
  • String values processed by application code are trimmed of leading and trailing whitespace and length-checked against a field-specific maximum before storage. Those maximums mirror the actual database column widths, so an oversized value is rejected at the application layer before it can ever reach the database.
  • All user-supplied strings rendered back to the browser are passed through HttpUtility.HtmlEncode() at the render site — not at the input boundary. Output encoding is applied where the data is consumed (the page rendering it), so a record that was inserted into the database before output encoding was applied at that location is still safely rendered now. Encoding is enforced at output rather than relying on input being scrubbed at insertion time.
  • The output-encoding rule applies uniformly to every user-controllable value rendered into HTML: trip names, note titles and bodies, location and address fields, tag labels, the IP address shown on the login page, and error messages echoed back from validation. Each rendering site invokes HttpUtility.HtmlEncode() on every field before writing it to the response.

Timeouts

  • HTTP request execution is capped at 120 seconds, preventing runaway requests from consuming server threads indefinitely.
  • Database queries time out after 30 seconds, preventing slow or hung queries from holding connections open.
  • Database connections time out after 30 seconds, limiting exposure to connection-pool exhaustion from unreachable servers.

Cookie & Token Handling

  • Authentication tokens and session IDs are never placed in URLs — they are stored in cookies only (cookieless="UseCookies"). This prevents tokens from appearing in server logs, browser history, address-bar copies, or HTTP Referer headers when following an outbound link.
  • The Forms Authentication ticket is protected with protection="All" — the cookie payload is both AES-encrypted (so its contents cannot be inspected) and HMAC-signed (so a single-byte modification invalidates the entire ticket). Tampering, partial forgery, and replay-after-decryption attempts all fail at the framework layer before any application code reads the ticket.
  • The Forms Authentication ticket carries the user ID only. No password, role, or other sensitive credential ever appears inside the cookie. Roles and profile data are loaded from the database on each request.
  • Cross-application authentication redirects are disabled (enableCrossAppRedirects="false"), preventing authentication tickets from being accepted by other applications on the same server or domain.
  • Both the session cookie (PLPSESSION) and the authentication cookie (.PLPAUTH) are set with SameSite=Lax. Browsers will not send these cookies on cross-site POST requests or with cross-site embedded resources, blocking a wide class of CSRF attacks at the cookie-delivery layer.
  • A site-wide default in <httpCookies> applies HttpOnly=true and SameSite=Lax to every cookie the application emits, including cookies created without an explicit policy. A developer cannot accidentally introduce a JavaScript-readable cookie by forgetting to set the flag.
  • Because TLS is terminated at the front-end proxy before the request reaches the application server — so the framework’s own Request.IsSecureConnection reads false on the back end — the application determines the true transport security from the X-Forwarded-Proto header via a single shared helper (SecurityHelper.IsRequestSecure). Every cookie the application sets directly (the post-login marker and all UI-preference cookies) receives the Secure flag whenever the originating public request was HTTPS, so it is never transmitted back over a plain-HTTP hop.
  • The cookie path is restricted to / for the auth and session cookies. The cookie scope does not inadvertently bleed into a different application path on the same host.
  • Expired session IDs are regenerated rather than reused (regenerateExpiredSessionId="true"), preventing session fixation attacks that rely on planting a known-expired identifier and waiting for it to be reissued.

Browser Cache Control

  • Every response served to an authenticated user carries a Cache-Control: no-cache, no-store directive along with a past Expires header. This prevents browsers and intermediate proxies from caching authenticated pages, so pressing the Back button after logging out cannot reveal previously viewed account data.
  • These cache directives are applied centrally in the master page’s Page_Load handler. Every page that uses the master — which is every authenticated page in the application — automatically receives them. A developer cannot accidentally introduce a cacheable authenticated page by forgetting to set them.

Rate Limiting

  • Rate limiting is applied independently to multiple sensitive operations, each with its own attempt threshold and time window: login, password reset, invitation sending, TripLink access, and the contact form.
  • All rate-limiting counters are stored in server-side memory. They cannot be reset by clearing cookies, opening a private browser window, or starting a new session.
  • The contact form applies two independent rate limits simultaneously: one keyed to the submitter’s IP address, and a second keyed to their authenticated user account. A user rotating IP addresses cannot bypass the per-account limit, and a shared network cannot exhaust the per-IP limit for all of its users simultaneously.

Client IP Detection

  • The IP address used for all rate-limiting decisions and audit-logging is resolved in a proxy-aware manner: the X-Forwarded-For request header is read first, enabling accurate client attribution when traffic passes through a load balancer or reverse proxy. The direct connection address is used as a fallback when no forwarding header is present. This ensures that throttle keys and security event logs attribute activity to the actual client rather than the intermediary infrastructure.

Search Engine & Crawler Controls

  • Sensitive pages — including login, trip data, notes, and account pages — carry noindex, nofollow directives. Search engines cannot index these pages or follow their links.
  • A robots.txt file explicitly instructs all search engine crawlers to avoid account and trip-related pages.

Error Handling

  • Friendly, generic error messages are shown to users — no technical or internal details are ever exposed.
  • Every unhandled exception is logged to the audit trail (plp_Logs) with full request context — the request URL, HTTP method, client IP, authenticated user, server-side session ID, and the complete exception chain including inner exceptions up to five levels deep — so failures can be investigated from the log without ever surfacing detail to the user.
  • Every server error generates a short correlation token that is shown to the user on the error page and written onto the matching log row (?cid=). A user can quote the token in a support request and the exact incident can be located in the audit log, without the error page ever exposing the underlying exception.
  • Any exception text that could ever reach a user is first passed through a dedicated redaction layer that strips absolute Windows and UNC file paths emitted by stack traces and removes connection-string fragments (Password=, User ID=, Server=, Data Source=). Even if a detailed message were surfaced through some other code path, it cannot leak a file-system layout or database credentials; the unredacted original is retained only in the server-side log.
  • As a last-resort diagnostic, every genuine unhandled exception is also appended to a server-side flat file (App_Data/last_errors.txt, append-only with automatic 5 MB rotation). This captures failures that occur after the HTTP response has already begun flushing — the point at which the normal database-log-plus-error-page path can no longer run or be seen. The file lives under App_Data, which IIS never serves over HTTP, and the entire routine is self-swallowing so it can never itself break a request.
  • 404 Not Found errors are treated as a distinct case: they are caught by the global error handler, logged as warnings with the requested URL and authenticated user context, and redirected to a generic error page. Internal file paths, directory structure, and the presence of protected files are never revealed in the response.
  • 401 Unauthorized HTTP responses are silently promoted to 403 Forbidden and redirected to the same generic error page. This prevents an attacker from distinguishing between “resource does not exist” and “resource exists but requires authentication” — the visible result is identical in both cases.

Security Logging

  • Security-relevant events are logged with IP address, browser information, and timestamp. Distinct severity levels (INFO, WARN, ERROR, CRITICAL, AUDIT, SECURITY) and category tags (Login, LoginFailed, AccountLocked, PasswordChange, PasswordReset, SecurityViolation, AccessDenied, Suspicious, Delete, Update, Exception, PageVisit, ClientData) make it possible to filter the audit trail to a specific event class.
  • When you visit key pages, your IP address, browser, device type, OS, screen resolution, language, and timezone are recorded. This information is used to help identify and respond to unauthorized access attempts and to reconstruct incidents from the audit trail.
  • Every log entry is enriched with structured fields parsed from the User-Agent — device type (mobile, tablet, desktop), browser family, and OS family/version — so the audit trail is filterable without requiring full-text User-Agent searches.
  • Every log entry is timestamped with the session ID, allowing the activity of a single session to be reconstructed even when the session has since been abandoned or expired.
  • All logging operations are wrapped in exception handlers — a failure to write a log entry cannot crash or degrade the application. The user’s operation continues even if the audit-trail database is briefly unavailable.
  • Log field values are individually truncated to their database column limits before insertion (e.g. IPAddress 45 chars, UserAgent 500 chars, PageURL 1000 chars, Message 2000 chars, and the free-text Details field at 32,000 chars). A pathological exception with a giant stack trace, or an oversized User-Agent, cannot fail the log write mid-statement, bloat a single row, or be used as a log-injection or buffer-style attack vector.
  • Audit-log writes are queued onto a background work item (HostingEnvironment.QueueBackgroundWorkItem) rather than executed inline, so the user-visible request is never held up by the log INSERT. The parameter values are fully materialised on the request thread before hand-off, so the background write never touches request state, and the runtime keeps the worker alive for in-flight log items across a graceful application restart. If the host is shutting down and cannot accept the work item, the write falls back to synchronous execution.
  • Contact identifiers that would otherwise be written verbatim into a log row are partially masked first (an email becomes a***@example.com, a phone becomes ***4567). The audit trail can still describe what was attempted without storing recoverable personal data where anyone with database read access could see it; the numeric user ID already on the same row is sufficient for follow-up.
  • Environment-capture log writes from anonymous visitors (on the home, join, and login pages) are themselves rate-limited per IP address, so an automated client repeatedly hitting those pages cannot flood the audit table with capture rows. Authenticated capture at sign-up always logs, since it is tied to a real, rare user milestone.

Log Entry Correlation

  • Every log entry can be assigned a unique CorrelationID (a randomly generated GUID) that links all log entries from a single request together. The complete sequence of events for any incident is reconstructable even when multiple log entries are written within a single page load and even when log writes are interleaved with concurrent traffic from other users.

Logout & Session Termination

  • Logging out is a multi-step process: FormsAuthentication.SignOut() revokes the authentication cookie, Session.Clear() removes all session data, and Session.Abandon() marks the server-side session for immediate destruction.
  • In addition to the framework calls, both the session cookie (PLPSESSION) and the authentication cookie (.PLPAUTH) are explicitly overwritten with empty values carrying a past expiration date. This belt-and-suspenders approach ensures client-side removal even on browsers that do not reliably honor cookie deletion via SignOut() or Abandon() alone.
  • The logout response itself carries Cache-Control: no-cache, no-store and a past Expires header, preventing the logged-out page from being cached.
  • The logout event is logged — including user ID and IP address — before the session is cleared, ensuring the audit trail is never lost.

Return URL & Redirect Safety

  • After login, the application redirects you only to a page within Packlist PRO. The redirect target is run through a dedicated whitelist check first: the path portion (everything before any ? query string or # fragment) must end in .aspx, and the value is rejected outright if it contains any whitespace or control character anywhere (closing the " //evil.com" leading-space trick that browsers normalise into a protocol-relative jump), if it begins with //, \\, /\, or \/, if it contains a backslash anywhere, or if it carries a URL scheme (a : appearing before the first /, ?, or # — the shape of http:, javascript:, or data:). A query string and fragment are deliberately preserved so a legitimate ?tripID=… survives the round-trip.
  • The “last page visited” stored in the database for post-login redirect is subject to the same validation rules as a query-string return URL. Values from the database are not trusted without passing the whitelist check.
  • All post-authentication redirects target relative paths within the application. It is not possible to redirect a user to an external domain through the login flow.

Previous Page Tracking Safety

  • The referring page is stored in the session to enable Back button navigation. Before storing it, the referrer host is compared to the current request host — only same-origin referrers are recorded. External referrers are silently discarded, preventing navigation state from being influenced by attacker-controlled external pages.
  • When a “Back” link is rendered from that stored value, a second guard (BasePage.ResolveBackUrl) accepts the target only if it is a local path (beginning with / or ~/) and is not the page you are already on; anything else falls back to the home page. An absolute or off-site URL can never become the destination of an in-page Back link — important for installed-PWA users who have no browser Back button.

Outbound Email Security

  • All outbound email is transmitted over an encrypted SMTP connection using TLS on port 587. The enableSsl setting in Web.config mandates encryption — plaintext delivery is not possible.
  • SMTP server credentials are read from Web.config at runtime via the framework’s system.net/mailSettings section. The SmtpClient instances used in application code do not contain hardcoded credentials.
  • The SMTP client timeout is explicitly capped at 15 seconds (overriding the framework default of 100 seconds). A slow or unreachable mail server cannot pin a request thread for an extended period, and an error-page render cannot stall behind a hung outbound notification.
  • Every email subject is passed through a header-sanitization step that strips carriage-return and line-feed characters and clamps the length before the message is built. A user-supplied value that flows into a subject line (for example a trip name) cannot inject additional SMTP headers.
  • Email send operations are wrapped in their own exception handlers. A mail-server outage cannot prevent the underlying user action from completing or the error page from being shown to the user.
  • Notification-class emails are sent on a background work item so the user’s request returns immediately; if a background send fails, the failure is logged with the recipient and message body deliberately omitted (bodies can carry one-time links and recipients are personal data) — only a fixed-format subject is recorded.
  • The locked-account alert email passes the offending username through HttpUtility.HtmlEncode before embedding it in the message body. An attacker who submits a malformed or markup-laden username cannot inject HTML into the alert.
  • Outbound email message and SMTP client objects are wrapped in Using blocks so the underlying network socket is closed immediately after the send completes, even if an exception bubbles up from the SMTP transport.

Output Encoding Discipline

  • Every user-controllable value that is rendered into HTML — trip name, note title and body, location and address fields, tag labels, user first name, the IP address shown on the login page, error messages echoed back from validation, and feedback type values — passes through HttpUtility.HtmlEncode() immediately before being written to the response.
  • Encoding is applied at the render site, not at insertion time. Even if a value was inserted into the database before encoding was enforced at that location (for example by a prior version of the code, an import script, or a developer using SSMS directly), the page rendering it still encodes the value safely.
  • Hand-built HTML fragments (trip list items, tag accordion buttons, ContactUs URL detection, AI metadata) explicitly call HttpUtility.HtmlEncode on every user-controllable substring before concatenation. There is no code path that writes a raw user value into rendered HTML.

Built-in Provider Isolation

  • The legacy ASP.NET built-in Membership and Role providers have been explicitly removed via <clear /> directives in Web.config. No application code or framework component can accidentally invoke default provider implementations, which carry weaker security assumptions and are not compatible with this application’s custom authentication model.
  • The ASP.NET RoleManager is disabled entirely (enabled="false"). Role decisions are made exclusively by application code reading from session data and the database — the framework’s built-in role infrastructure plays no part.

Database Connection Management

  • The database connection pool is capped at 500 connections (Max Pool Size=500 in the connection string). This places a hard upper bound on simultaneous database connections and prevents a traffic burst from exhausting the SQL Server connection limit.
  • All database commands and connections are opened and closed inside Using blocks, guaranteeing that connections are returned to the pool immediately after use — even if an exception occurs mid-query.
  • Result-set reads are performed through a SqlDataAdapter that fills a disconnected DataTable or DataRow and then immediately releases the underlying connection back to the pool. The application never holds an open server-side cursor across application code, so a slow or stalled caller cannot pin a database connection.
  • The connection string sets TransparentNetworkIPResolution=False, preventing the .NET SQL client from performing additional DNS resolution attempts that could add latency or fail silently in some network environments.
  • The connection string is read once at class-load time from the ConnectionString entry in <connectionStrings>. There is no code path that constructs a connection string from concatenated input, so a malicious value cannot influence which database, server, or credentials are used.
  • Every command (ExecuteNonQuery, ExecuteScalar, ExecuteDataTable, ExecuteDataRow) exposes a ParamArray of SqlParameter objects. Calling code constructs queries with @Name placeholders and binds values through parameters; there is no overload that accepts string interpolation, so a developer cannot accidentally introduce a concatenated query.
  • Nullable values are routed through dedicated helpers (Param, ParamNullableInt, ParamNullableDate) that explicitly convert Nothing to DBNull.Value. A null value cannot become an empty string or a literal “Nothing” in a generated SQL statement.
  • Reader and row accessors (GetInt, GetString, GetBool, GetNullableDate) check for DBNull and missing columns before reading. A schema change that drops a column does not throw inside the access layer — it returns the documented default, preventing a partial deployment from cascading into a runtime exception.

CSRF Protection for AJAX, WebMethods, and Handlers

  • Every authenticated request issued by the page receives a per-session CSRF token, generated as a cryptographically random 32-character GUID and stored only in server-side session memory. The token is exposed to client JavaScript through a single inline assignment (window.plpCsrfToken) injected by the base page at render time; it is never written to a cookie, never embedded in a URL, and never reflected into the DOM as an attribute.
  • The shared AJAX helper (PLP.callMethod) reads window.plpCsrfToken on every call and forwards it as a custom X-PLP-CSRF request header. Every state-changing WebMethod (trip save, trip permissions, trip notes, trip delete, trip select, trip duplicate, note email, etc.) and every HTTP handler (NoteService.ashx) invokes SecurityHelper.ValidateCsrfToken() as its first action — before authentication is even checked, before any database query runs.
  • Token comparison is performed in constant time. The validator XORs each character of the submitted header against the stored session token and accumulates the bit-difference, returning equality only after walking the full string — never short-circuiting on the first mismatched character. This prevents the timing side-channel that a length- or byte-position-sensitive comparison would expose to a network-adjacent attacker.
  • Cross-origin AJAX delivery of the token is impossible: the X-PLP-CSRF header is not a CORS-safelisted header, so any cross-origin fetch or XMLHttpRequest that attempts to set it triggers a CORS preflight. The server never answers that preflight with an Access-Control-Allow-Origin grant, so the browser blocks the request before the actual call is ever sent.
  • When a CSRF token goes stale — for example after a server restart recycles the session while a tab is still open — the server tags the rejected response with an X-PLP-CSRF-Status: expired header. The shared AJAX helper detects the tag, transparently fetches a fresh token, and replays the original call exactly once. A token that simply aged out is recovered without the user ever seeing a spurious “invalid request” error, and the replay is hard-capped at a single attempt so a genuinely forged request cannot loop.
  • The token-refresh endpoint is the one component deliberately exempt from CSRF validation — requiring the token in order to fetch the token would deadlock the recovery path — but it is otherwise tightly bounded: it requires an authenticated session, is POST-only so the token never lands in a URL or referrer, returns the token only to same-origin script that the browser already gates, and is rate-limited to 20 requests per user per minute, with a breach of that limit logged as a security violation.
  • Every CSRF validation failure — whether the header is missing, the session token is missing, or the values do not match — is recorded as a SecurityViolation event with the requesting user ID and the request path, so probing attempts surface in the audit trail even when the endpoint silently returns “Invalid request.”
  • The token comparison logic is itself wrapped in a try/catch that swallows any exception from the logging path. A logging-layer outage cannot crash the CSRF validator and cannot leak via an unhandled exception.
  • Login and Contact Us forms each maintain their own page-scoped CSRF token, regenerated on every form load and rotated again immediately after each submission. A captured form post cannot be replayed even within the same session, even within the same minute.

Cryptographic Discipline

  • Every per-session secret — the AJAX CSRF token, the per-form login CSRF token, the per-form contact-us CSRF token, the trip sharing TripLinkCode, and the per-request CorrelationID where used — is generated from Guid.NewGuid(), which under .NET on Windows draws from the OS cryptographic random source (BCryptGenRandom). No security-relevant identifier is derived from a counter, timestamp, or predictable seed.
  • Temporary passwords issued by the account-recovery flow are generated with a cryptographic random number generator (System.Security.Cryptography.RandomNumberGenerator), never the non-cryptographic System.Random. The alphabet deliberately omits the visually ambiguous glyphs 0/O and 1/l/I so the value can be transcribed from an email or text message without confusion, and a minimum length is enforced in code regardless of the requested length.
  • Internal cache keys for cached AI responses use SHA-256 over the model name and full prompt text — and, for answers whose correctness depends on the date, the current UTC date is folded into the hashed input so a date-sensitive response is never served stale from a prior day. The 256-bit hash space prevents prefix-collision games and means a malformed prompt cannot collide with a legitimate cached response.
  • Writes into the AI-response cache are guarded by a WHERE NOT EXISTS check on the cache key, so two requests computing the same answer at the same moment cannot insert duplicate or interleaved rows for a single key.
  • ViewState integrity uses the framework’s default HMAC algorithm with the machine key. No application code attempts to roll its own MAC or hash for ViewState; the framework primitive is used exactly as designed.
  • The framework’s cryptographic keys are explicitly pinned in configuration (<machineKey> with validation="HMACSHA256" and decryption="AES") rather than being auto-generated per worker. Pinned keys mean a MAC-signed ViewState payload, an encrypted Forms Authentication ticket, or a protected cookie stays valid across application-pool recycles and is verified identically by every worker process — there is no key-rotation window for an attacker to exploit, and because the keys are unique to this site a ticket minted here is never accepted by any sister application.

Configuration Whitelisting

  • Every user-selectable preference value is validated server-side against an explicit whitelist before persistence: ColorScheme must be exactly “L” or “D”; SiteMode must be 1, 2, or 3; DetailLevel must be 1, 2, or 3; FontSize must be 14, 16, or 18. Any other value falls back to a safe default rather than being written through — a tampered dropdown post cannot smuggle an out-of-range value past the database CHECK constraints.
  • The Contact Us feedback category is validated against a fixed string list (bug, quality, enhance, feature, security, access, support, abuse, other). Tampered or unexpected values are rejected before the database insert or the outbound email send.
  • The pre-select query-string parameter that lets a deep-link to Contact Us pre-fill the feedback category is validated against the same whitelist before being applied to the dropdown. A crafted URL cannot inject an arbitrary value into the rendered form.
  • Where comma-separated UserID lists appear in application settings, each token is parsed as a positive integer through Integer.TryParse. Non-integer tokens, negative values, and the literal string zero are silently dropped so a typo in the configuration file cannot accidentally widen the set.

Time Handling

  • All timestamps written to the database (note created, note updated, trip created, trip updated, login, logout, lockout, log entry, AI cache entry, file upload) are stored as UTC via GETUTCDATE(). The audit trail and concurrency tokens are unaffected by daylight-saving transitions or server time-zone changes.
  • UTC values are converted to the user’s local time zone only at render time, using a TimeZoneInfo.Id the user explicitly set on their profile. Conversion is performed inside a try/catch — if the stored time-zone ID is unrecognised (for example, when the underlying OS time-zone database has been updated), the display falls back to server-local time rather than throwing a rendering exception.
  • Time-zone lookups are routed through a process-wide cache that records both successful and failed resolutions. A malformed or unknown stored time-zone ID is resolved once, its negative result is cached, and every later row carrying the same bad ID skips both the registry lookup and the exception — so a single corrupt profile value cannot amplify into a storm of repeated throws across a long list render or the reminder batch.

Login Page Security

Rate Limiting

  • Login attempts from a single IP address are rate-limited. After too many failures, further attempts from that address are temporarily blocked.
  • Unusual volumes of failed logins from a single IP address — a pattern characteristic of credential stuffing attacks — are detected and flagged. An alert email is sent to the site operator when the detection threshold is crossed, but only once per throttle window per IP address — a deduplication flag prevents repeated alerts from the same ongoing attack.
  • The throttle window and attempt thresholds are configurable in the server-side application settings, not hardcoded.

Account Lockout

  • After a number of consecutive failed password attempts, your account is temporarily locked.
  • The lock expires automatically — no action is required on your part to unlock your account.
  • A warning is shown before the lockout threshold is reached, giving you a chance to recover access.
  • When an account is locked out, an alert email is sent to the site operator, including the username and the number of failed attempts that triggered the lockout.

Credential Protection

  • Failed login attempts show the same error message whether the username or the password was wrong. This prevents attackers from discovering which usernames exist on the platform.
  • After a successful login, your old session is destroyed through a two-step process: Session.Abandon() releases the server-side session container, and the session cookie (PLPSESSION) is simultaneously overwritten with an empty value bearing a past expiration date. Both steps are required because Abandon() alone does not guarantee client-side cookie removal on all browsers.
  • A brand-new session and session ID are issued to you after login. An attacker who obtained your pre-login session ID cannot use it after authentication.
  • The password field is never pre-populated from any stored source.
  • After login, you are only redirected to pages within the Packlist PRO application — open redirect attacks are not possible.
  • Passwords are never trimmed before comparison. A password that begins or ends with a space is stored and verified exactly as entered.
  • The username and password fields each have a server-side maximum length of 200 characters. Inputs that exceed these limits are rejected before any database query is issued.
  • The account lockout check runs before the password comparison. A locked account is rejected immediately without comparing the submitted password against the stored value — eliminating a timing side-channel that could otherwise allow an attacker to confirm that a locked account holds the correct password.
  • After a correct password is matched, the login flow performs a secondary status check: the account must be both active and approved before the session is established. An account with the correct password is still rejected if its Active or Approved flag is unset. These two states are checked independently of the lockout mechanism, so toggling either flag immediately blocks subsequent logins without waiting for password expiry or session rotation.
  • The account lookup fails closed on ambiguity. If a username ever resolves to more than one account row — for example after a manual database edit that bypassed the uniqueness constraint — the login is refused outright and recorded as a security violation, rather than authenticating against whichever row the database happened to return first.
  • The failed-attempt counter is incremented with a single atomic database statement (UPDATE … OUTPUT INSERTED.FailedAttempts). Two simultaneous failed attempts cannot each read a stale count and slip past the lockout threshold — the database serialises the increment.
  • If you are already signed in — a valid session or a still-valid Forms Authentication ticket resolves your account — a request for the login page does not re-render the credential form at all; you are redirected straight to a validated in-app destination. An already-authenticated user cannot be lured into re-entering their credentials on a stale or planted login link.

Form Security

  • A one-time CSRF (Cross-Site Request Forgery) token is embedded in the login form and validated on every submission. The token is regenerated after each use, so replaying a captured form submission is not possible.
  • The CSRF token is a cryptographically random identifier — it is not predictable, sequential, or guessable.
  • The failed-attempt counter used for rate limiting is stored in server-side memory — it cannot be reset by clearing browser cookies, switching browsers, or opening a new session.
  • The CSRF token is stored in server-side session memory, not in a cookie or any client-accessible location — a third-party site cannot read or replay it.

Credential Stuffing Counter Isolation

  • The credential stuffing detection counter and the per-IP login throttle counter are tracked under separate server-side cache keys. A successful login clears the IP throttle counter but does not clear the stuffing counter. An attacker rotating through a stolen credential list from a shared IP address cannot reset the stuffing alert threshold by occasionally succeeding with a valid pair.
  • The stuffing alert deduplication flag is stored under its own cache key with the same expiry window as the throttle. Only one alert email is sent per IP address per window — subsequent failed attempts from the same IP during the same window do not generate additional notifications.
  • The stuffing detection threshold is configurable via the ThrottleLoginStuffing_Threshold application setting — it is not hardcoded and can be adjusted without a deployment.

Audit Logging

  • Every login attempt — successful or failed — is logged with the IP address, browser, and timestamp.
  • Your IP address, browser, device type, OS, screen resolution, language, and timezone are recorded when the login page loads.
  • Each failed login attempt records a precise LastFailedAttemptAt timestamp on the account row in addition to the audit log entry. This provides a separate forensic timeline directly on the account, independent of the central log table.
  • The IP address displayed on the login page is HTML-encoded before rendering, preventing a specially crafted IP header from being used as a reflected XSS vector.
  • Successful login resets both the failed-attempt counter on the account and the IP-based throttle counter in server memory, so a legitimate user’s IP address is not blocked from future logins after a successful authentication.

Browser Integration

  • The password field uses TextMode="Password" so the entered value is masked in the browser’s rendered DOM. Shoulder-surfing of the typed password is mitigated at the input layer.
  • The username and password fields carry standard HTML autocomplete hints (autocomplete="username" and autocomplete="current-password"). These hints allow modern password managers to fill credentials correctly while signalling to the browser that no other field should be confused for a password.
  • The login form’s default submit button is set explicitly to the Login button so pressing Enter never accidentally triggers a different page action.

Notes Page Security

Access Control

  • Every note operation — loading, saving, and deleting — independently verifies that you own the trip or have been granted access via a TripLink. Access is not assumed from the page having loaded successfully.
  • Private notes are visible only to the user who created them, even when two trips are linked via a TripLink code.
  • Only the creator of a note may delete it, even if the note has been marked as shared and is visible to others. The Delete button is only shown to the creator in the interface, and the server enforces the same rule independently — these are two separate checks.
  • A shared note’s visibility setting (shared or private) can only be changed by its original creator. Other users who have edit access to the note’s content cannot alter its visibility.
  • The note-loading service only accepts HTTP POST requests — GET and other methods are rejected before any note data is accessed.
  • Note data responses include a Cache-Control: no-store directive, preventing browsers and intermediate proxies from caching sensitive note content.

Data Integrity

  • Notes use optimistic concurrency control: each note carries a version stamp. If two people attempt to save the same note at the same time, the second save is rejected with a clear message rather than silently overwriting the first person’s changes.
  • All note database operations use parameterized queries.
  • Before the version stamp is used in a database query, it is validated to be exactly 8 bytes — the size SQL Server expects for rowversion values. Malformed tokens are rejected without reaching the database.

Session Verification

  • The service that loads note data performs its own independent session and access check, separate from the page’s own authorization check. There is no single point of failure in the authorization chain.
  • The note-loading handler implements IRequiresSessionState, so the ASP.NET runtime guarantees the session is attached before ProcessRequest begins. A request that arrives without a valid session is rejected at the framework boundary rather than partway through application code.
  • The handler validates its CSRF header before parsing the request body, so a CSRF-failed request never causes the application to deserialise attacker-controlled JSON. Parsing errors on a CSRF-valid body fall through to a generic “Invalid request” response that does not echo any of the submitted content.

Cross-Trip Visibility Rules

  • The note-fetch query implements three explicit visibility rules in a single parameterised statement: (1) the note belongs to the requesting user on the currently selected trip; (2) the note is marked shared and belongs to the currently selected trip; (3) the note is marked shared and belongs to a different trip whose TripLinkCode matches the current trip’s TripLinkCode. Each clause is filtered on the joined trip’s ownership, so a TripLinkCode collision from a non-linked third-party trip cannot smuggle a note into the response.
  • The TripLinkCode comparison uses the empty-string short-circuit (AND @TripLinkCode <> '') so a trip with no sharing code in place cannot be matched against another trip’s missing/empty code by accident.

Note Email Safety

  • The “email these notes to me” action validates the CSRF token, confirms trip ownership, and then sends only to the address registered on the requester’s own account — it accepts no caller-supplied recipient address. A tampered request cannot turn the feature into an open relay for mailing note content to an arbitrary third party.
  • Every note title and body is HTML-encoded into the outbound message, so note content cannot inject markup into the email that is delivered.

Note Update Access Predicate

  • A note update succeeds only when the same SQL statement confirms the requester either owns the note or has shared access to its trip (directly, or through a matching share code, re-checked by a correlated subquery). A forged note ID paired with a valid version stamp cannot update a note belonging to a trip the requester has no access to — the access predicate is part of the UPDATE itself, not a separate check that could be skipped.

Trip & Sharing Security

Access Control

  • Every trip data operation — loading, saving, and deleting — passes through an application-layer ownership check. Additionally, every UPDATE and DELETE statement includes the current user’s ID in its WHERE clause, so a bug in the application-layer check could not lead to data being modified: the database operation would affect zero rows rather than another user’s trip.
  • The application-layer ownership check is performed by a dedicated VerifyOwnership helper that runs a parameterized SELECT against plp_Trips filtered by both TripID and UserID. Save, share-permission, note, duplicate, and delete operations each call this helper before proceeding — no operation skips it.
  • The TripID held in your session is re-verified against actual database ownership on every sensitive operation. It is not assumed to be valid because it was set when you logged in.
  • Unauthorized access attempts — such as passing a TripID that belongs to another user — are logged as security violations with your user ID, IP address, and the TripID you attempted to reach.
  • A duplicate-key violation on save (SQL Server error 2601 or 2627) is caught explicitly and surfaced as a user-friendly duplicate-name message. Raw SQL exception text is never echoed to the user.

TripLink Sharing

  • Trip sharing is implemented through a TripLinkCode: a randomly generated 16-character uppercase string derived from a version-4 GUID. The code is not predictable or sequential — it cannot be guessed through enumeration. The 16-character alphanumeric (uppercase) keyspace exceeds 1.2×1024 possible values.
  • A database unique constraint enforces that no two trips share the same TripLinkCode. This is enforced at the storage layer independently of application logic. Even an application bug that re-used a code would be rejected at the database level.
  • If a newly generated share code ever collided with an existing one, the insert is retried up to three times with a freshly generated code before failing, so an astronomically rare collision never surfaces to the user as an error. A uniqueness violation on any other column (such as a duplicate trip name) is correctly distinguished and reported as its own user-friendly message.
  • TripLink code access is rate-limited: repeated access attempts from a single IP address are blocked after a configurable threshold, preventing automated enumeration of valid codes.
  • Only the trip owner is shown or can use the TripLinkCode. It is not exposed to users who have been granted shared access.
  • When a trip is duplicated, a brand-new TripLinkCode is generated for the copy — the duplicate does not inherit the original’s code. Anyone who previously had the original’s TripLink cannot reach the duplicate through that link, and the trip owner gets a fresh, separately shareable code.
  • Duplicating a trip copies only its name, items, tasks, and tags. The trip dates, location and descriptive fields, the Retrospective and Lessons-Learned notes, and all sharing-permission flags are intentionally reset on the copy (sharing flags are hard-set to off). A duplicate therefore never inherits the original’s sharing exposure or its private retrospective content — sharing a copy is always a fresh, deliberate choice.

Sharing Permissions

  • Trip sharing uses per-feature permission flags: separate settings for expense data, packing list read access, and task list read access. The trip owner chooses exactly which features to share — sharing is not all-or-nothing.
  • Sharing permissions can only be changed by the trip owner. Users with shared access cannot modify what they are permitted to see.

Trip Deletion

  • When a trip is deleted, all associated records — packing items, tasks, notes, tag assignments, and sharing links — are removed by database-level cascade delete constraints. No orphaned trip data can remain, and no data can be re-associated with a future trip after deletion.
  • If the deleted trip was the currently selected trip in your session, it is immediately cleared from the session to prevent stale references.

Input Validation

  • Trip date ranges are validated server-side: if both a start date and an end date are provided, the end date must not precede the start date.
  • All string fields — trip name, description, location, city, state, postal code, and country — are checked against explicit maximum lengths that match the actual database column sizes. Oversized values are rejected before reaching the database.
  • Trip names must be non-empty. A blank trip name is rejected with a validation error before any database operation is attempted.

Shared Trip Notes

  • Notes marked as shared are visible to other users with trip access, but only the note’s original creator can change whether a note is shared or private. This flag is enforced by the UPDATE statement itself — the SQL uses a CASE expression that preserves the original value if the requesting user is not the note’s owner, regardless of what the client submits.
  • Private notes — those not marked for sharing — are never visible to other users, even when two trips are linked.

Tag Permission Filtering

  • When a trip is saved with a set of tag selections, the submitted tag IDs are not trusted. Each ID is intersected server-side against the set of tags the user is permitted to use — public tags (IsPublic = 1) plus tags they personally created (AddedByUserID = @UID). Any submitted ID outside that set is silently dropped before the database write.
  • The intersection uses a single parameterised SELECT…WHERE TagID IN (…) against the integer-validated set, so a tampered submission cannot attach another user’s private tag to a trip and thereby leak the private tag’s label on the trip dashboard.
  • Tag IDs submitted by the client are first filtered to positive integers and deduplicated before being assembled into the IN clause. Zero, negative, and duplicate IDs are removed at the application layer before reaching the database.

Transactional Tag Updates

  • Saving a trip’s tag set is a destructive-and-rebuild operation: every existing plp_TripTags row for the trip is deleted before the new set is inserted. Both the DELETE and every INSERT execute inside a single explicit SQL Server transaction. If any INSERT fails, the transaction rolls back and the trip retains its original tag set — there is no failure mode in which a trip ends up partially tagged or tag-less.
  • The transaction is committed only after every INSERT has succeeded. A client that closes its connection mid-request triggers a rollback by the server, not a partial commit.

Session-Aware Trip Deletion

  • When a trip is deleted, the application checks whether the deleted TripID matches the currently selected TripID in your session. If so, the session value is cleared immediately so subsequent navigation cannot dereference a deleted ID and either show stale data or trigger an access-denied error.
  • The persisted LastTripID in plp_SitePreferences is also cleared in the same operation. On your next login, the application will not attempt to auto-select a trip that no longer exists — an auto-selection would surface as an empty page with no clear remediation path.

Trip Selection Verification

  • The lightweight “select this trip” endpoint that runs when you click a trip in the Trips list re-verifies ownership against the database before updating your session. Setting Session("TripID") is not a side-effect of clicking a link — it requires the database to confirm the trip belongs to you (or is shared with you) on that exact request.
  • On every page load of EditTrip.aspx, the TripID present in the session is re-verified against actual ownership before the trip’s fields are loaded into the form. If the trip no longer exists or no longer belongs to you, the session value is cleared and you are bounced back to the trip list rather than shown an empty or partially populated edit form.

Print-Page Access Control

  • The print-friendly versions of your packing list, task list, and notes do not assume the page that linked to them already authorised you. Each one independently marks its response no-cache / no-store with a past Expires, resolves your identity from session or the Forms Authentication ticket (redirecting to login if you are anonymous), validates the requested trip ID as a positive integer (404 otherwise), and runs its own SELECT COUNT(*) … WHERE TripID = @TripID AND UserID = @UserID ownership check (403 otherwise) before a single row of trip data is loaded.

Account & Session Security

Session Rebuild After Interruption

  • ASP.NET InProc sessions are lost if the server’s application pool restarts. When this happens, BasePage detects the missing session and automatically reconstructs it from your still-valid Forms Authentication ticket by re-querying your user profile, role, and preferences from the database. You remain logged in and your work is not interrupted.
  • The session rebuild process only succeeds when a valid, non-expired, cryptographically intact authentication ticket is present. A missing, expired, or tampered ticket results in an anonymous session, not a reconstructed one.
  • When a session is rebuilt from a still-valid ticket, your Active and Approved flags are re-read from the database in the same query. If either is unset, the ticket is revoked (FormsAuthentication.SignOut()) and the request continues anonymously rather than reconstructing the session. A deactivated account cannot keep itself logged in for the remaining life of its long-lived ticket — deactivation takes effect on the very next request.
  • Session rebuilds are logged at the INFO level with the user ID and timestamp. Unusual patterns — such as repeated rebuilds from a single account within minutes — are visible in the audit trail for forensic review.

Remote Sign-Out (Sign Out Everywhere)

  • Each signed-in device carries a per-device security-stamp cookie (HttpOnly). On every session rebuild the device’s stamp is compared against the account’s current stored stamp; if you rotated the stamp from another device, this device’s still-valid ticket stops matching and is bounced to sign-in. This is what lets a single “sign out everywhere” action terminate every other logged-in session even though the authentication ticket itself is long-lived.
  • The comparison is fail-open by construction — it only ever rejects when both a device cookie and a stored stamp are present and differ — so a missing or not-yet-issued stamp can never force an unexpected logout.

Preference & UI State Cookies

  • Cookies used to persist UI preferences (such as detail level and filter settings) are set with HttpOnly=true, preventing client-side JavaScript from accessing them. They are also set with Secure=true when the connection is HTTPS, preventing them from being transmitted over plain HTTP.

Authentication Re-verification on Every Operation

  • Authentication state is re-evaluated on every page load and every AJAX operation. There is no page or endpoint that relies on a check performed during a previous request.
  • When a WebMethod or AJAX handler is called and the in-memory session is cold (such as after an app pool restart), the user identity is re-derived from the still-valid Forms Authentication ticket via HttpContext.Current.User.Identity.Name. The framework populates this principal regardless of session state, so AJAX operations remain authenticated through transient infrastructure events without forcing the user to re-log in.
  • The cold-session fallback restores Session("UserID") from the validated FormsAuth principal so subsequent calls in the same logical session no longer have to repeat the fallback.
  • The fallback only succeeds when the FormsAuth ticket is present, decryption succeeds, and the embedded identity parses as a positive integer matching a known user. A missing, expired, or tampered ticket results in an unauthenticated response, not a recovered session.

Client Environment Data Collection

  • The browser-set cookies that record screen resolution (plp_screen) and time zone (plp_tz) carry only display-related telemetry. They contain no identity, no authentication token, and no permission flag. The cookies are SameSite=Lax and path-restricted to the site.
  • Cookie values are URL-encoded on the client and URL-decoded on the server before storage. A specially crafted screen-resolution or time-zone string cannot inject control characters into a log entry or downstream consumer.
  • The server-side logger reads these values from cookies first, falls back to session storage, and finally to current-request form fields. The cookie path makes the values available on every request type (GET, POST, AJAX) without requiring the client to resubmit them.

Anonymous Access Boundaries

  • Anonymous users can view read-only content such as the home page and public information pages. Any attempt to perform a write operation — saving data, creating a trip, or modifying preferences — redirects to the registration page rather than silently failing or partially succeeding.
  • The demo account’s trips are available to anonymous users through a dedicated code path that verifies trip ownership against the demo account on every access. Anonymous users cannot reach other users’ trips through the demo pathway.
  • The DemoUserID is configured via application settings, not hardcoded into application logic. The demo account can be re-pointed without a code change, and the anonymous boundary is enforced by reading this setting at runtime rather than by comparing against compiled constants.
  • On demo-account login, the packlist/tasklist display preferences are reset to safe defaults. A previous demo-session user’s display state cannot influence the experience of the next demo-session user.
  • The demo-account cookies that carry the reset preferences are written with HttpOnly=true, so even a script injected through a third-party means cannot read or alter them through JavaScript.

Authenticated User Resolution Order

  • The shared user-resolution helper (SecurityHelper.GetAuthenticatedUserID) follows a strict priority order: (1) read Session("UserID") if present and parses to a positive integer; (2) otherwise, read the Forms Authentication ticket via HttpContext.Current.User.Identity.Name and require the ticket to be present, decrypted, and to parse as a positive integer. There is no third fallback — no cookie value, no query-string parameter, no client-supplied header is consulted for identity.
  • When the slow path (FormsAuth fallback) succeeds, the resolved UserID is restored into Session("UserID") so subsequent calls within the same request use the fast path. This prevents the cold-session penalty from compounding across multiple AJAX calls in a single page-load burst.
  • The fallback returns 0 (anonymous) rather than throwing when the ticket cannot be validated. Endpoints that receive 0 return the standard “session expired” response, identical to the response a never-authenticated client would receive. The two cases are indistinguishable to a probing attacker.

Quicklist & Data Browsing Security

Access Control

  • The Quicklist page requires authentication. Unauthenticated users are redirected to the registration page before any data is loaded.
  • The tag list shown to each user is filtered by ownership: only publicly available tags and tags created by the current user are returned. It is not possible to enumerate another user’s private tag collection.
  • Quicklist results are scoped the same way for the rows themselves: both items and tasks must be either public (IsPublic = 1) or owned by the current user. A private item or task belonging to another user is never returned even when it shares a selected tag — the ownership guard on the result rows mirrors the one on the tag list.

Tag ID Validation

  • Tag IDs submitted as filter selections are individually parsed and validated as integers before being assembled into SQL IN clauses. Any value that does not parse to a valid integer is discarded before it reaches the database. This prevents non-integer input from influencing the query, even though such values cannot be parameterized in an IN list.

Preference Cookie Security

  • Filter preferences (detail level, checkbox states) are saved to cookies so your settings persist across visits. These cookies are set with HttpOnly=true and Secure=true (on HTTPS connections) — they cannot be read by JavaScript and are not transmitted over unencrypted connections.
  • Cookie values that do not match expected formats (integers or known boolean strings) fall back to safe defaults rather than propagating unexpected input into the application.

Contact Form Security

CSRF Protection

  • The contact form is protected by a session-based CSRF token that is independent of the login form’s token — each sensitive form on the site maintains its own token. The token is embedded in the rendered page, validated on every submission, and immediately rotated. A captured form submission cannot be replayed.
  • The CSRF token is stored in server-side session memory, not in a cookie or any client-readable location. A third-party site cannot read or replay it.

Rate Limiting

  • Contact form submissions are throttled simultaneously by two independent limits: one keyed to the submitter’s IP address, and a second keyed to their authenticated user account. A user rotating IP addresses cannot bypass the per-account limit, and a shared network cannot exhaust the per-IP limit for all of its users simultaneously.
  • The throttle thresholds and time windows for the contact form are independently configurable in application settings, separately from the login and password-reset throttles.

Bot Detection

  • The contact form includes a honeypot field — a hidden input rendered inside a visually-hidden container marked aria-hidden="true", removed from keyboard navigation with tabindex="-1", and excluded from autofill with autocomplete="off". Human users do not see, focus, tab to, or fill this field.
  • If the honeypot field contains any value at submission time, the request is silently discarded, the suspicious event is logged with the IP address and a privacy-safe fingerprint of the value (its length and whether its first character is alphanumeric) — never the raw content, since automated tools frequently dump stolen credentials into hidden fields — and the submitter is redirected to the same success page a legitimate user would see. Bots receive no feedback that they were detected and no signal that would help refine their evasion attempt.
  • The bot-rejection path bypasses the database insert entirely. Bot submissions never reach plp_Feedback and never trigger an outbound email or file-storage operation.

File Attachment Allowlist & Size Limits

  • Attached files are restricted to an explicit server-side allowlist of image types: .jpg, .jpeg, .png, .gif, .webp, .bmp, .tiff, .tif, .heic, and .heif. Any other file extension is rejected before the file is read or stored.
  • Individual file size is capped at 10 MB. Total combined size across all attachments is capped at 20 MB. No more than 10 files may be attached to a single submission. All three limits are enforced server-side before any file processing begins. The per-file extension check, per-file size check, total-size check, and file-count check are each performed independently, so an exploit must defeat every layer to succeed.

File Signature (Magic Byte) Verification

  • An allowlisted extension is necessary but not sufficient. The first bytes of every uploaded file are inspected and matched against the binary signature for the declared type before the file is written to disk. A .png with executable bytes, a .jpg wrapping HTML, or any other content-type/extension mismatch is rejected outright.
  • Per-type signature checks include JPEG (FF D8 FF at offset 0), PNG (89 50 4E 47 at offset 0), GIF (47 49 46 38 at offset 0), WebP (RIFF at offset 0 with WEBP at offset 8), BMP (42 4D at offset 0), TIFF (little-endian 49 49 2A 00 or big-endian 4D 4D 00 2A at offset 0), and HEIC/HEIF (ISOBMFF ftyp box: 66 74 79 70 at offset 4).
  • Files that fail signature verification are not just rejected silently — they are logged at SECURITY severity with the filename, declared extension, and FeedbackID for later review.
  • The file’s input stream is rewound after the header read so subsequent processing receives the file from byte zero. The signature inspection has no side effect on the saved file.

Filename Sanitization & Path Hardening

  • The uploaded filename is passed through Path.GetFileName() before any other processing. Directory path components in the submitted filename (such as ..\\..\\Windows\\System32) are stripped at the framework layer, preventing path-traversal attacks at write time.
  • Following path stripping, the filename is reduced to alphanumeric characters, dashes, and underscores. Every other character — including dots inside the name portion — is replaced with a single underscore. Consecutive underscores are collapsed. This eliminates double-extension attacks (such as image.png.exe) and shell-meaningful characters in stored names.
  • Sanitized base names are capped at 80 characters. The original extension is preserved but lowercased, so a stored .PNG cannot disguise itself from an extension-based scanner that expects lowercase.
  • Stored filenames are prefixed with the database-generated FeedbackID, guaranteeing that two submissions can never collide on disk even if their original filenames were identical, and providing a stable cross-reference between the stored blob and the database record.
  • The destination path is computed by joining the application’s mapped ~/ContactUsUploads/ directory with the sanitised filename through Path.Combine(). Even if the sanitiser were bypassed, the resulting absolute path is constrained to a single fixed directory because every component that could re-introduce a path-traversal sequence has already been stripped.
  • Empty filenames that result from sanitising a name that contained no valid characters are replaced with the literal “file” before the extension is reattached. There is no code path that writes a file with an empty base name.

Upload Directory Sandboxing

  • The ContactUsUploads/ folder that stores feedback attachments carries its own scoped Web.config whose authorization rule denies direct URL access to every user (<deny users="*" />). The stored attachments are not reachable by guessing or browsing a path — there is no public URL that serves a file out of this folder at all.
  • The same scoped Web.config strips the folder’s IIS handler registrations and re-adds only the static-file handler, so even if an attacker were to somehow place a .aspx, .ashx, .cgi, or .php file there — by defeating the extension check, magic-byte check, and filename sanitisation simultaneously — IIS would refuse to invoke any script handler against it.
  • Retrieval of a stored attachment flows exclusively through an authorization-gated handler that validates the requested identifier as a positive integer, resolves the file inside the canonical upload directory, and rejects any resolved path that escapes that directory. Path-traversal in the lookup cannot reach a file outside the sandbox, and the raw folder is never exposed.
  • That same handler computes the canonical absolute path of both the upload directory and the requested file and streams the file only when the resolved path begins with the canonical directory — a runtime second line of defence against ..\ traversal even if a tampered name ever reached disk. It answers only GET and HEAD requests (any other verb is refused), and the stored filename is stripped of carriage-return, line-feed, and double-quote characters before it is written into the Content-Disposition response header, closing the header-injection vector a crafted filename would otherwise open.

Outbound Link Surveillance

  • The comment body of every submission is scanned for http:// and https:// URLs. Any URL whose host is not on a curated domain allowlist is recorded as a suspicious-content event with the user ID, IP address, the offending URL list, and the first 500 characters of the comment for context.
  • The host comparison strips port numbers, lowercases the host, and matches both exact-domain and subdomain forms. A URL using a port suffix or an obscure subdomain still evaluates against the same allowlist.
  • Detection is logging-only, not blocking. Legitimate users are never prevented from submitting feedback because they linked to an external article or screenshot host; the audit trail simply preserves which submissions warrant a closer look.

Input Validation

  • The feedback category (bug report, feature request, general feedback, and so on) is validated against a server-side whitelist of known-valid values before the submission proceeds. A manipulated or unexpected category value is rejected before it reaches the database or the outbound email system.
  • Email addresses, when provided, are parsed via the framework’s System.Net.Mail.MailAddress constructor and then compared byte-for-byte against the original input. A value that the parser silently normalises (a common technique for slipping past lazy regex validation) is rejected because the comparison fails.
  • Email and phone fields are individually capped at 254 characters (the IETF maximum email length). Oversized values are rejected before any database write or outbound email send.
  • The comment body is capped at 4000 characters server-side, matching the underlying database column size.
  • The IP address of every submitter is captured and persisted alongside the feedback record itself, providing a per-submission audit trail without depending on the central log table.

Progressive Web App Security

Cross-Origin Request Isolation

  • The Packlist PRO service worker intercepts all outgoing requests made by the app. Any request directed at a domain other than Packlist PRO’s own origin is passed through to the network without being handled by the service worker. This prevents the service worker from caching, intercepting, or serving content from third-party origins.
  • The cross-origin check is the first line of the fetch handler — before strategy selection, before path matching, before any cache lookup. There is no execution path in which a cross-origin response can enter the service worker’s cache.

HTTP Method Restriction

  • The service worker only intercepts GET requests. All other HTTP verbs (POST, PUT, DELETE, PATCH) bypass the service worker entirely and pass straight to the network. A state-changing request can never be served from cache, replayed from cache after an offline period, or cached as a side effect of a previous identical post.
  • The verb check is performed before any other routing decision, so even a same-origin POST to an asset path cannot accidentally pick up a cached response.

API Path Exclusion

  • Any request whose path begins with /api/ is excluded from service worker handling regardless of method or origin. This namespace is reserved for future server APIs and is intentionally inaccessible to caching, so an API call cannot be silently replayed by the worker after an offline period.

Network-First Strategy for Data Pages

  • Pages that display trip data, packing lists, and task lists use a network-first cache strategy: the live server response is always requested first, and a cached copy is used only if the network is unreachable. This ensures users always see current data rather than a stale cached snapshot, and prevents a cached page from being served across different authenticated sessions.

AJAX and API Responses Not Cached

  • The service worker does not cache AJAX calls or HTTP handler responses. All requests that return JSON data — including WebMethod calls and .ashx handler endpoints — are excluded from service worker caching entirely. Sensitive server responses from these endpoints are never written to browser storage.

Static Asset Caching

  • Static resources (CSS, JavaScript, and images) use a cache-first strategy with a network fallback. Because these files are served with version identifiers, a cached copy accurately reflects a known-good deployment state and cannot be silently replaced with unexpected content.

Cache Version Management

  • Each service worker deployment carries a unique cache version identifier. On activation, the service worker deletes all cache buckets from prior versions. Assets from a previous deployment cannot persist after an update and cannot be served to users of the current deployment.

Offline Fallback

  • When a user is offline and requests a page that is not in the cache, the service worker returns a dedicated offline page rather than allowing the browser to display a generic network error. The offline page contains no application data or authenticated content — only a status message and a reload control.
  • Service worker registration is wrapped in a feature-detection check. Browsers that do not support service workers continue functioning normally over the network without any cached-content code path. Failed registration in a permissive environment (such as a non-HTTPS development server) is silently swallowed and never displayed to the user.

Client-Side & AJAX Defenses

Session Expiry Detection

  • Every AJAX call made through the shared PLP.callMethod() helper inspects the HTTP status. A 401 Unauthorized response (returned by the framework when the auth cookie is missing or expired) triggers a clear, modal “session expired” prompt rather than failing silently.
  • The prompt offers two paths: re-authenticate (the return URL is preserved as the current page so the user lands back where they left off) or remain on the page in view-only mode. View-only mode disables every save and delete button on the page, preventing the user from accidentally attempting to submit a change that would silently fail under an expired session.
  • Callers that supply their own error handler can override the prompt — for example, a background save uses the override to surface its own toast rather than interrupting the user with a modal — but the silent-failure path is opt-in, not the default.

Offline-Aware Saves

  • Every AJAX call checks the connection state before attempting to send. If the browser is offline, the call is short-circuited locally and the supplied error handler receives a localized offline message immediately, without making a doomed network request that would otherwise time out.
  • That connection state does not trust the notoriously flaky navigator.onLine flag on its own. An offline event is first debounced for three seconds, then confirmed with a HEAD request to /favicon.ico (a tiny, authentication-free, no-store probe) carrying a four-second timeout; the connection is treated as down only when the browser flag and the failed probe agree. This prevents spurious “you’re offline” blocks during cell-tower handoffs while still short-circuiting a genuinely doomed save before it is sent.
  • Save buttons share a single offline-guard helper. A save attempted while offline triggers a toast rather than a confused half-state where the UI shows “saved” but the server never received the request.
  • An online/offline event listener toggles an offline class on document.body, allowing CSS to surface a clear visual indicator that the user is disconnected. There is no scenario in which the user appears connected but their input is being lost.

Save-Button Lifecycle

  • Buttons marked with the data-save-btn attribute are intercepted by a centralized click handler that disables the button and replaces its text with a spinner immediately on click. This prevents accidental double-submission of forms and AJAX calls during network latency.
  • The save lifecycle exposes three explicit terminal states — saved, error, and disabled (offline) — each represented by a distinct CSS class. A reader of the page state always sees the canonical outcome rather than a stale “Saving…” spinner left behind by an interrupted handler.

In-Flight Request Coalescing

  • The shared AJAX helper keys every call by its URL plus its serialized request body and coalesces duplicates: a second identical call made while the first is still in flight does not open a second network request — it registers its callbacks against the pending one and is notified when that settles. A rapid double-click, or two components triggering the same save at once, therefore cannot double-fire a state-changing WebMethod at the transport layer, complementing the per-button disable-on-click guard above.

Destructive-Action Confirmation

  • Elements annotated with data-confirm raise a confirmation dialog before their default action proceeds. The dialog message is taken from the attribute value, so each destructive action carries context-specific wording rather than a generic prompt. A cancelled confirmation halts the click propagation entirely, preventing the underlying delete or destructive postback from running.

Local Storage Resilience

  • All reads from and writes to localStorage and sessionStorage are wrapped in exception handlers. Browsers in private/incognito mode or with storage permissions denied throw on access; the application gracefully falls back to non-persistent behaviour rather than crashing or refusing to render.
  • Filter and preference values read back from storage are validated against the page’s known option set before being applied. A tampered storage value cannot inject an unexpected control state.

Per-Request CSRF Header Injection

  • The shared AJAX helper reads the per-session CSRF token from a single global variable (window.plpCsrfToken) the base page injects on every render. On every call it sets the X-PLP-CSRF request header. Every state-changing endpoint validates this header before doing any work; an AJAX call from another tab, another origin, or a clipboard-injected snippet that does not run inside the original page cannot read the variable and therefore cannot present a valid token.
  • The AJAX helper deliberately omits the X-Requested-With: XMLHttpRequest header. ASP.NET 4.5+ uses that header to trigger an automatic 401 response for AJAX calls that hit an auth check, which would break WebMethod-style endpoints. Omitting it lets the application return clean JSON for both authenticated and session-expired states, which the client interprets through the explicit 401 detection path documented above.
  • The CSRF token is rotated whenever the session is destroyed (login success, logout, or a manual Session.Abandon()). Any captured token from a previous session cannot be replayed against a fresh session.

Session-ID Regeneration on Authentication

  • After a successful login, the framework’s session is explicitly abandoned and the session cookie is overwritten with an empty value carrying a past expiration. The browser receives a fresh PLPSESSION identifier on the next request. Any pre-authentication session ID known to an attacker (for example, planted by linking the victim to a session-fixation URL) cannot be promoted to an authenticated session.

Request Timeout

  • Every AJAX call carries a 30-second client-side timeout that mirrors the server’s database command timeout. A hung server or dropped connection fires the error handler and resets the originating control, rather than leaving the user staring at an indefinite “saving” spinner with no resolution.

No Sensitive Data in Local Storage

  • The only values written to localStorage or sessionStorage by the application are scroll-position integers and UI preference strings (dropdown selections, filter toggles). No authentication token, no session identifier, no email address, no trip data, no AI prompt, and no AI response is ever cached client-side.

Transport & Platform Hardening

Transport Layer Encryption

  • The site is served exclusively over HTTPS in production. The HTTP Strict Transport Security (HSTS) header instructs every modern browser that has previously connected to the site to refuse plain-HTTP downgrades for one year, covering the apex domain and every subdomain.
  • Outbound SMTP (port 587) and outbound HTTPS to external services use the operating system’s TLS stack with current cipher suites. The application never opens a plaintext socket to an external service.

Server Fingerprint Reduction

  • The IIS X-Powered-By header, the ASP.NET X-AspNet-Version header, and the framework version banner (enableVersionHeader="false" in <httpRuntime>) are all suppressed. Automated scanners cannot fingerprint the platform from response headers.
  • Detailed IIS error pages are restricted to local connections only (httpErrors errorMode="DetailedLocalOnly"). Remote clients receive the generic application error page even when an unhandled IIS-level error occurs.

Layered Error Routing

  • HTTP status codes 404 and 500 are routed to the application’s generic error page through two independent mechanisms: the ASP.NET <customErrors> block in system.web handles errors raised inside application code, while the IIS <httpErrors> block in system.webServer handles errors raised before application code runs (such as a request for a file that doesn’t exist).
  • The framework’s global error handler intercepts every unhandled application exception, logs the full context to the audit trail, and routes the user to the same generic error page that the IIS layer would use. The user-visible outcome is identical regardless of which layer caught the error.

Maintenance-Mode Gate

  • A maintenance-mode gate runs in Application_PostAuthenticateRequest — after the authentication ticket is parsed but before session acquisition or any page handler — so when enabled it diverts every request type (full pages, AJAX WebMethods, and .ashx handlers) to a maintenance page that returns HTTP 503 with a Retry-After hint, not just pages that use the master layout. The static assets the maintenance page itself depends on are exempted so it always renders correctly.

Scheduled-Job Authentication

  • The background reminder job is invoked through an unattended HTTP endpoint (Handlers/RunReminders.ashx) that authenticates the caller with a shared secret supplied in a custom X-Reminder-Token request header. The submitted value is compared against the configured secret in constant time — the length difference is folded in and the full string is walked — so the comparison leaks no timing signal about how many leading characters were correct.
  • The endpoint accepts only POST; a stray GET is rejected with HTTP 405 and logged. Restricting the verb keeps the secret out of access logs, browser history, and Referer headers, and the responses are marked no-store / no-cache so no intermediary retains the job output.
  • The job fails closed: if the expected secret is missing or blank in configuration the request is refused with HTTP 503 rather than running unauthenticated, and the misconfiguration is logged as an error. A missing or mismatched token is rejected with HTTP 401 and recorded as a security violation with the client IP — and neither the submitted token nor the expected token is ever written to the log, so a near-miss value can never leak through the audit trail.

Static Folder Authorisation

  • Asset folders (Content/, Scripts/, Images/) are explicitly opened to anonymous access through scoped <location> elements in Web.config. Authentication-restricted folders inherit the application’s default deny-anonymous posture.
  • Framework internals (App_Code/, App_Data/, bin/, obj/, SQL/) are explicitly listed in robots.txt as Disallow paths and are not served by IIS handlers. Source code, database scripts, and compiled binaries cannot be retrieved over HTTP.
  • The legacy ASP.NET membership provider, role manager provider, and profile provider entries are explicitly cleared from configuration. No framework path can fall back to a default provider implementation; every authentication and role check goes through application-controlled code.

Built-in Compilation Posture

  • VB.NET compilation runs with Option Strict off but Option Explicit on. Every variable must be declared before use; implicit declaration cannot accidentally introduce a typo-driven security gap (such as a misspelled session key that resolves to Nothing and silently bypasses an access check).
  • The compiler targets .NET Framework 4.8. The selected runtime supports the cryptographic primitives (HMAC, AES-GCM, SHA-256) the application relies on for ViewState, ticket protection, and cache keying.

Database-Layer Defenses

Parameterised Query Discipline

  • Every database call in the application flows through a single centralised access layer (DatabaseHelper) whose every public method requires a parameterised SQL string and a parameter array. There is no convenience overload that accepts a pre-built string, so there is no “shortcut” path for concatenated SQL to enter the codebase.
  • SQL strings in application code use named placeholders (@TripID, @UserID, etc.) that are bound through SqlParameter instances. The SQL Server client driver type-checks and length-checks each parameter against the column it binds to before transmission.
  • Dynamic IN-clauses (such as the Quicklist tag filter) parse and validate every value as an integer before any value reaches the SQL string. A value that does not parse to a positive integer is dropped before query assembly, so non-numeric content cannot influence the query plan even though IN-list values cannot themselves be parameterised.

Search Normalization Symmetry

  • Punctuation-insensitive search routes both your typed term and the database column it is matched against through one shared character-strip set, so the two can never drift apart and silently fail to match — a search for “Beer Wine” still finds an item stored as “Beer & Wine”. A single helper defines the stripped characters; both the application-side normalisation and the SQL-side REPLACE chain are generated from it.
  • The normalised search term is wrapped in wildcards and bound to the query as a @Search parameter rather than concatenated into the SQL text. Only the fixed, developer-defined column expression is inline; your term never enters the SQL string, so a punctuation-insensitive search offers no LIKE-injection foothold.

Schema-Driven Validation

  • Field length limits enforced server-side — for trip name, description, location, city, state, postal code, country, note title, note body, comment body, email address, phone number, and similar fields — match the actual NVARCHAR column sizes defined in SQL/CreateTables.sql. The application’s rejection criteria are kept in sync with the database’s storage limits, so oversized input never reaches the database layer.
  • Numeric and date fields are parsed through Integer.TryParse / DateTime.TryParse before being passed as parameters. A non-numeric or non-parseable date value is rejected at the application layer with a user-friendly message rather than being shipped to the database as a malformed string.
  • Trip date validation is symmetric: when both a leaving date and a return date are provided, the return date must not be earlier than the leaving date. The check uses normalised dates after parsing, so it cannot be defeated by submitting different surface formats for the two values.

Duplicate-Key Surfacing

  • SQL Server unique-constraint violations (errors 2601 and 2627) on trip names and note titles are caught explicitly and re-surfaced as a user-friendly “already exists” message. The underlying SqlException message — which can contain server name, database name, and constraint name — is never echoed to the user.
  • The same explicit handling applies to TripLinkCode and other uniqueness-bound columns, so a near-impossible random collision still results in a clean rejection rather than a generic 500 error.

Server-Side Time Authority

  • Every row that is timestamped at insert (notes, trips, lockouts, logins, log entries, AI cache entries, file uploads) takes its DateAdded / DateUpdated / CreatedAt value from GETUTCDATE() on the SQL Server itself, not from the application server’s clock and not from any client-supplied value. Clock drift between the web tier and the database tier cannot create a window in which audit timestamps disagree, and a client cannot back-date a record by forging a hidden field.
  • Account-lockout expiration is computed server-side via DATEADD(MINUTE, @Min, GETUTCDATE()) in the same statement that sets the lockout. The window length cannot be extended or shortened by clock skew on the calling server.

Optimistic Concurrency

  • Notes carry a SQL Server rowversion column that is fetched alongside the note body when a note is loaded for editing. On save, the UPDATE statement filters on the original rowversion value — a second saver whose snapshot is stale sees zero rows affected and is shown a clear “this note was changed by someone else” message rather than silently overwriting the other person’s edit.
  • The OUTPUT clause on the same UPDATE returns the new rowversion in a single round-trip, so a user who saves successfully can continue editing without re-loading the note. The token round-trip preserves the concurrency guarantee across consecutive saves in the same session.
  • The submitted rowversion bytes are validated server-side to be exactly 8 bytes (SQL Server’s storage width for rowversion). A malformed or arbitrary-length token is rejected before any database query runs.

Cascade Integrity

  • Foreign-key relationships from child tables (trip items, trip tasks, trip notes, trip tags) to plp_Trips are defined with ON DELETE CASCADE. Deleting a trip removes every associated child row atomically; no orphaned trip data can remain.
  • The cascade is enforced at the database layer, not the application layer. Even a direct DELETE issued outside the normal application flow cleans up dependent rows, so a maintenance script removing a trip cannot accidentally leave behind orphaned packing items.

Index-Backed Authorisation Queries

  • The most frequent authorisation query (verify TripID is owned by the current UserID) is supported by the clustered PK on plp_Trips(TripID) plus a covering index on plp_Trips(UserID). Link-based sharing uses the filtered index on plp_Trips(TripLinkCode). Authorisation checks remain O(log n) even as the dataset grows, so per-operation checks remain affordable and the application does not feel pressure to skip or cache them.

Defense-in-Depth Posture

Layered Authorisation

  • A single state-changing trip operation passes through up to four independent permission checks before any data is modified: (1) the AJAX endpoint validates the CSRF token; (2) it re-derives the authenticated user from session or the Forms Authentication ticket; (3) it calls VerifyOwnership against the trip table with both TripID and UserID; (4) the UPDATE statement itself includes WHERE UserID = @UID. Any single layer being bypassed by a future code change still leaves three independent layers in place.
  • Authorisation queries are constructed so that an unauthorised TripID does not produce a database error or expose row counts — the query simply returns zero rows and the operation reports “not found or access denied,” identical to the response the client receives for a non-existent TripID. The two cases cannot be distinguished by response timing or message content.

Layered Input Defense

  • A single user-supplied string passes through up to five independent defenses before being rendered: (1) ASP.NET request validation rejects obvious script-injection patterns at the framework boundary; (2) the application trims the value and length-checks it against the field’s maximum; (3) the database NVARCHAR column refuses values that exceed its declared length; (4) the rendering site calls HttpUtility.HtmlEncode immediately before writing to the response — the primary XSS defense; (5) the response carries a Content-Security-Policy whose object-src 'none', form-action 'self', frame-ancestors 'self', and base-uri 'self' directives sharply narrow what any markup that somehow survived encoding could reach — it cannot embed a plugin object, repoint a form to an external collector, frame the page, or hijack relative URLs through an injected <base> tag.

Layered Transport Defense

  • HTTPS exclusivity is enforced by three independent mechanisms: (1) the front-end Plesk SSL terminator redirects plain HTTP to HTTPS at the network edge; (2) the HSTS header instructs browsers to refuse plain-HTTP downgrades for one year, covering every subdomain; (3) the Forms Authentication ticket and session cookie are configured for SameSite=Lax to prevent cross-site delivery even on misconfigured intermediaries.

Layered Error Containment

  • An unhandled application exception is contained by four independent mechanisms: (1) Global.asax Application_Error intercepts the exception, logs it with full context and a correlation token, and redirects to the generic error page; (2) the ASP.NET <customErrors> block routes any error that escapes the application handler to the same generic error page; (3) the IIS <httpErrors> block catches errors that occur before application code runs and routes them identically; (4) detailed error pages are restricted to local connections only, so remote clients see the generic page even if every preceding layer fails. The four mechanisms produce visually identical responses, so the user cannot distinguish which layer caught the failure.

Independent Failure Domains

  • The logging, email-notification, and audit-write paths are each wrapped in their own exception handlers. A failure in any one of these subsystems is silent to the user and does not prevent the user’s original action from completing — if the audit log database is briefly unavailable, the user still saves their trip; if the SMTP server is unreachable, an account-lockout or contact-form notification fails silently while the user’s action still completes; if the toast-message script registration fails, the inline alert panel still displays the message.

Symmetric Failure Responses

  • Failed login messages do not distinguish between “username does not exist” and “password incorrect.” The lockout check runs before the password check so the timing of both branches is comparable. The visible error message, the HTTP status code, and the response timing are intentionally identical — an attacker cannot enumerate which usernames exist on the platform.
  • Trip-access denied and trip-not-found responses are identical from the user’s perspective. An attacker cannot determine whether a TripID exists at all by probing the endpoint with arbitrary numeric values.