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.