Tips

  • This page describes the security measures Packlist PRO uses to protect your account and data.
  • Each card below covers a different layer — site-wide protections, authentication, and your privacy.
  • Have a security concern? Report it via Contact Us.

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 — explicitly disables browser APIs the application does not use (geolocation, microphone, camera). Even a fully compromised script running on the page cannot prompt for access to these capabilities.
  • 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' blocks all third-party origins by default; connect-src 'self' blocks cross-origin AJAX/fetch calls; 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 and X-AspNet-Version response headers are removed from every response, preventing automated scanners from identifying the server technology stack.
  • 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.
  • 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 passed through a centralized sanitization step (SecurityHelper.Sanitize) that trims leading and trailing whitespace and truncates the value to a field-specific maximum length before storage. This normalization is applied uniformly via a shared helper rather than ad hoc per page, ensuring consistent enforcement across all input paths.
  • 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, error messages echoed back from validation, and feedback text rendered for moderation. 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.
  • 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 triggers an immediate email notification to the site operator, including the request URL, HTTP method, user IP address, session ID, and the full exception chain. This ensures failures are investigated before users need to report them.
  • The error notification handler is itself wrapped in error handling — a failure to send the alert email cannot prevent the error page from being shown to the user.
  • 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). Oversized inputs cannot trigger a database error mid-log-write and cannot be used as a log-injection or buffer-style attack vector.

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 to the page you were trying to reach. The redirect target is validated before use: it must consist only of alphanumeric characters, dots, and underscores; it must end with .aspx; and it must not begin with // (the protocol-relative prefix used in open redirect attacks).
  • 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.

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.
  • Exception notification emails include the full request URL, HTTP method, client IP address, User-Agent string, authenticated user ID, server-side session ID, and the complete exception chain including all inner exceptions (up to five levels deep). This provides enough context to fully reconstruct an incident without requiring direct server log access.
  • Exception notification emails are suppressed when the application is running on a localhost hostname. Development errors never trigger live alerts.
  • 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.
  • 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 that arrives in the operator’s inbox.
  • 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.
  • Streaming queries use CommandBehavior.CloseConnection, ensuring the underlying connection is closed as soon as the SqlDataReader is disposed, even if the result set is not fully consumed by the caller.
  • 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, ExecuteReader) 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, GetDateTimeNullable) 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 preflight that the server’s connect-src 'self' CSP and absence of a cross-origin allow-list rejects.
  • 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.
  • Internal cache keys for cached AI responses use SHA-256 over the model name and full prompt text. The 256-bit hash space prevents prefix-collision games and means a malformed prompt cannot collide with a legitimate cached response.
  • 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.

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 (privileged-tester exemptions for the contact form), 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 exemption 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.

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.

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.

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.
  • 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.

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, notes, tag assignments, sharing links, and preferences — 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.

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.
  • 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.

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 trip is available to anonymous users through a dedicated code path. Anonymous users cannot reach other users’ trips through the demo trip pathway.
  • The DemoUserID and DemoTripID are configured via application settings, not hardcoded into application logic. The demo target can be re-pointed without a code change, and the anonymous boundary is enforced by reading these settings at runtime rather than by comparing against compiled constants.
  • On demo-account login, the LastTripID and packlist/tasklist display preferences are reset to safe defaults. A previous demo-session user’s 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.

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 that is positioned off-screen (position:absolute;left:-9999px), measured at 1px×1px, marked aria-hidden="true", given tabindex="-1" to remove it from keyboard navigation, and given autocomplete="off" so password managers ignore it. 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 the value submitted, 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 5 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 that strips every IIS handler registration with <clear /> and re-adds only the StaticFileHandler for the GET and HEAD verbs. Even if an attacker were to somehow place a .aspx, .ashx, .cgi, or .php file in that folder — for example, by defeating every layer of the extension check, magic-byte check, and filename sanitisation simultaneously — IIS would refuse to invoke any script handler against it and would serve the raw bytes as a static download.
  • The same scoped Web.config applies a request-filtering rule with <verbs allowUnlisted="false"> that allows only GET and HEAD. Every other verb (POST, PUT, DELETE, PATCH, OPTIONS, PROPFIND, MKCOL, etc.) is rejected with 404 by IIS before any handler runs. There is no HTTP-visible mechanism to write or modify content in the upload directory.

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 navigator.onLine 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.
  • 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.

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.

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, dispatches the operator alert email, 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.

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.

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 and length-truncates via SecurityHelper.Sanitize; (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; (5) the response carries a Content-Security-Policy that blocks inline event-handler execution from any value that survived encoding.

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, emails the operator, 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, the user still sees a clean error page; 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.