Site-Wide Security
Data Protection
- All database queries use parameterized statements, which prevents SQL injection attacks.
- User-provided content is HTML-encoded before being displayed, preventing cross-site scripting (XSS) attacks.
- Server-side validation is performed on all inputs independently of any client-side validation.
- Field length limits are enforced server-side against actual database column sizes.
Authentication & Session Management
- Authentication uses an encrypted and cryptographically signed Forms Authentication ticket.
- If your session data is lost (for example, after a server restart), it is automatically rebuilt from your valid authentication ticket — you remain logged in without re-entering your credentials.
- Sessions use non-default cookie names to reduce automated targeting.
- The authentication ticket is decrypted inside an error-handling block — malformed or tampered cookies are silently discarded rather than causing an application error.
- Session data is loaded from the database once per request and held in memory for that request’s lifetime, reducing round-trips and ensuring all code within a single request sees consistent values.
Access Control
- Authorization is verified independently on every sensitive operation, not just when a page first loads.
- Authentication state is re-evaluated on every page load and every AJAX call. There is no endpoint that trusts a check performed during a prior request.
- Trip ownership and sharing permissions are checked on every read, write, and delete operation.
Security Headers
The following security headers are sent with every response:
- X-Content-Type-Options: nosniff — prevents browsers from guessing content types, stopping MIME-sniffing attacks that could cause a non-HTML response to be interpreted and executed as a script.
- X-Frame-Options: SAMEORIGIN — prevents the site from being embedded in iframes hosted on other origins, protecting against clickjacking and UI-redress attacks.
- Referrer-Policy: strict-origin-when-cross-origin — sends only the origin (not the full URL) to third-party sites when you follow a link, preventing path and query-string data from leaking to external destinations.
- Permissions-Policy — denies a broad set of powerful browser APIs the application never uses:
geolocation, microphone, camera, payment, usb, and the motion sensors (magnetometer, gyroscope, accelerometer). Even a fully compromised script running on the page cannot prompt for or silently access any of these capabilities. The same header additionally sets interest-cohort=() and browsing-topics=(), opting the site out of Google’s FLoC / Topics advertising-cohort APIs so visitors are never enrolled in interest-based ad profiling as a side effect of using the site.
- Cross-Origin-Opener-Policy: same-origin — severs the
window.opener reference between this site and any page on a different origin that opens it (or that it opens), closing the cross-window scripting and tab-nabbing channel that an attacker-controlled popup would otherwise use to reach back into the application’s browsing context.
- Cross-Origin-Resource-Policy: same-origin — instructs the browser to refuse to embed Packlist PRO responses as a subresource on any other origin, blocking cross-origin resource inclusion and the speculative side-channel reads (Spectre-class) that such inclusion enables.
- Strict-Transport-Security (HSTS): max-age=31536000; includeSubDomains — instructs browsers to use HTTPS exclusively for one full year, covering the entire packlistpro.net domain and every subdomain. Once received, the browser refuses plain-HTTP downgrades even if a user attempts to type an
http:// URL.
- The HSTS header is applied conditionally — only on responses served over a verified HTTPS connection, and never on localhost requests. This prevents a developer’s local environment from being permanently locked to HTTPS-only.
- Content-Security-Policy (CSP) — restricts where browser content can be loaded from.
default-src 'self' establishes the site’s own origin as the baseline for every content type; connect-src limits AJAX/fetch/WebSocket targets to the site itself plus the push-notification provider’s endpoints (no other cross-origin network destination is reachable); frame-ancestors 'self' reinforces the X-Frame-Options block at the modern-CSP layer; form-action 'self' prevents any form on the site from being repointed to submit user data to an external domain; base-uri 'self' prevents a <base> tag from being injected to relocate every relative URL; and object-src 'none' blocks embedding plugin objects (Flash, Java applets, generic <object>/<embed> tags) altogether.
- The
X-Powered-By, X-AspNet-Version, and IIS Server response headers are removed from every response (the Server banner is stripped both by requestFiltering removeServerHeader="true" and an explicit code-level removal), preventing automated scanners from identifying the server technology stack from the response banner.
- Security headers are emitted by two independent mechanisms: the IIS
<httpProtocol> block in Web.config (for production IIS) and the Application_BeginRequest handler in Global.asax (for IIS Express and other development environments). Both layers must fail simultaneously for a header to be missing from a response.
ViewState & Form Integrity
- All ViewState is cryptographically signed (Message Authentication Code) and encrypted (
viewStateEncryptionMode="Auto"). The encrypted, MAC-protected ViewState cannot be read by an attacker and cannot be modified without invalidating the signature.
- EventValidation is enabled site-wide. The framework records the set of legitimate postback target IDs and event values when a page is rendered, and rejects any postback containing an event from a source not present in that original set. Attempts to fabricate postback events for hidden or disabled controls are rejected at the framework level before any application code runs.
- ViewState is bound to your current session via
ViewStateUserKey, which is set in the OnInit stage of every page — the earliest valid point in the page lifecycle. ViewState submitted in one user’s session cannot be replayed by another user, even if the raw bytes are captured.
- ViewState encryption is applied to every page site-wide — it is not opt-in per page.
Input Filtering
- ASP.NET request validation (
validateRequest="true") rejects any form submission containing potentially dangerous HTML or script characters before it reaches application code.
- ASP.NET response-header checking is enabled (
enableHeaderChecking="true" in <httpRuntime>). The framework encodes carriage-return and line-feed characters in any value written to a response header, neutralising HTTP response-splitting and header-injection attempts in which attacker-controlled data — a redirect target, a cookie value — would otherwise inject a second header or a forged response body.
- Incoming request body size is capped at approximately 26 MB server-side by two independent layers operating in agreement: the ASP.NET
maxRequestLength setting in <httpRuntime> and the IIS-level requestLimits.maxAllowedContentLength setting in <system.webServer>. The lower of the two values controls. Both must be misconfigured before an oversized request can reach application code. Requests that exceed the limit are rejected by the framework before any application code runs, preventing oversized-payload denial-of-service attacks.
- String values processed by application code are trimmed of leading and trailing whitespace and length-checked against a field-specific maximum before storage. Those maximums mirror the actual database column widths, so an oversized value is rejected at the application layer before it can ever reach the database.
- All user-supplied strings rendered back to the browser are passed through
HttpUtility.HtmlEncode() at the render site — not at the input boundary. Output encoding is applied where the data is consumed (the page rendering it), so a record that was inserted into the database before output encoding was applied at that location is still safely rendered now. Encoding is enforced at output rather than relying on input being scrubbed at insertion time.
- The output-encoding rule applies uniformly to every user-controllable value rendered into HTML: trip names, note titles and bodies, location and address fields, tag labels, the IP address shown on the login page, and error messages echoed back from validation. Each rendering site invokes
HttpUtility.HtmlEncode() on every field before writing it to the response.
Timeouts
- HTTP request execution is capped at 120 seconds, preventing runaway requests from consuming server threads indefinitely.
- Database queries time out after 30 seconds, preventing slow or hung queries from holding connections open.
- Database connections time out after 30 seconds, limiting exposure to connection-pool exhaustion from unreachable servers.
Cookie & Token Handling
- Authentication tokens and session IDs are never placed in URLs — they are stored in cookies only (
cookieless="UseCookies"). This prevents tokens from appearing in server logs, browser history, address-bar copies, or HTTP Referer headers when following an outbound link.
- The Forms Authentication ticket is protected with
protection="All" — the cookie payload is both AES-encrypted (so its contents cannot be inspected) and HMAC-signed (so a single-byte modification invalidates the entire ticket). Tampering, partial forgery, and replay-after-decryption attempts all fail at the framework layer before any application code reads the ticket.
- The Forms Authentication ticket carries the user ID only. No password, role, or other sensitive credential ever appears inside the cookie. Roles and profile data are loaded from the database on each request.
- Cross-application authentication redirects are disabled (
enableCrossAppRedirects="false"), preventing authentication tickets from being accepted by other applications on the same server or domain.
- Both the session cookie (
PLPSESSION) and the authentication cookie (.PLPAUTH) are set with SameSite=Lax. Browsers will not send these cookies on cross-site POST requests or with cross-site embedded resources, blocking a wide class of CSRF attacks at the cookie-delivery layer.
- A site-wide default in
<httpCookies> applies HttpOnly=true and SameSite=Lax to every cookie the application emits, including cookies created without an explicit policy. A developer cannot accidentally introduce a JavaScript-readable cookie by forgetting to set the flag.
- Because TLS is terminated at the front-end proxy before the request reaches the application server — so the framework’s own
Request.IsSecureConnection reads false on the back end — the application determines the true transport security from the X-Forwarded-Proto header via a single shared helper (SecurityHelper.IsRequestSecure). Every cookie the application sets directly (the post-login marker and all UI-preference cookies) receives the Secure flag whenever the originating public request was HTTPS, so it is never transmitted back over a plain-HTTP hop.
- The cookie path is restricted to
/ for the auth and session cookies. The cookie scope does not inadvertently bleed into a different application path on the same host.
- Expired session IDs are regenerated rather than reused (
regenerateExpiredSessionId="true"), preventing session fixation attacks that rely on planting a known-expired identifier and waiting for it to be reissued.
Browser Cache Control
- Every response served to an authenticated user carries a
Cache-Control: no-cache, no-store directive along with a past Expires header. This prevents browsers and intermediate proxies from caching authenticated pages, so pressing the Back button after logging out cannot reveal previously viewed account data.
- These cache directives are applied centrally in the master page’s
Page_Load handler. Every page that uses the master — which is every authenticated page in the application — automatically receives them. A developer cannot accidentally introduce a cacheable authenticated page by forgetting to set them.
Rate Limiting
- Rate limiting is applied independently to multiple sensitive operations, each with its own attempt threshold and time window: login, password reset, invitation sending, TripLink access, and the contact form.
- All rate-limiting counters are stored in server-side memory. They cannot be reset by clearing cookies, opening a private browser window, or starting a new session.
- The contact form applies two independent rate limits simultaneously: one keyed to the submitter’s IP address, and a second keyed to their authenticated user account. A user rotating IP addresses cannot bypass the per-account limit, and a shared network cannot exhaust the per-IP limit for all of its users simultaneously.
Client IP Detection
- The IP address used for all rate-limiting decisions and audit-logging is resolved in a proxy-aware manner: the
X-Forwarded-For request header is read first, enabling accurate client attribution when traffic passes through a load balancer or reverse proxy. The direct connection address is used as a fallback when no forwarding header is present. This ensures that throttle keys and security event logs attribute activity to the actual client rather than the intermediary infrastructure.
Search Engine & Crawler Controls
- Sensitive pages — including login, trip data, notes, and account pages — carry
noindex, nofollow directives. Search engines cannot index these pages or follow their links.
- A
robots.txt file explicitly instructs all search engine crawlers to avoid account and trip-related pages.
Error Handling
- Friendly, generic error messages are shown to users — no technical or internal details are ever exposed.
- Every unhandled exception is logged to the audit trail (
plp_Logs) with full request context — the request URL, HTTP method, client IP, authenticated user, server-side session ID, and the complete exception chain including inner exceptions up to five levels deep — so failures can be investigated from the log without ever surfacing detail to the user.
- Every server error generates a short correlation token that is shown to the user on the error page and written onto the matching log row (
?cid=). A user can quote the token in a support request and the exact incident can be located in the audit log, without the error page ever exposing the underlying exception.
- Any exception text that could ever reach a user is first passed through a dedicated redaction layer that strips absolute Windows and UNC file paths emitted by stack traces and removes connection-string fragments (
Password=, User ID=, Server=, Data Source=). Even if a detailed message were surfaced through some other code path, it cannot leak a file-system layout or database credentials; the unredacted original is retained only in the server-side log.
- As a last-resort diagnostic, every genuine unhandled exception is also appended to a server-side flat file (
App_Data/last_errors.txt, append-only with automatic 5 MB rotation). This captures failures that occur after the HTTP response has already begun flushing — the point at which the normal database-log-plus-error-page path can no longer run or be seen. The file lives under App_Data, which IIS never serves over HTTP, and the entire routine is self-swallowing so it can never itself break a request.
- 404 Not Found errors are treated as a distinct case: they are caught by the global error handler, logged as warnings with the requested URL and authenticated user context, and redirected to a generic error page. Internal file paths, directory structure, and the presence of protected files are never revealed in the response.
- 401 Unauthorized HTTP responses are silently promoted to 403 Forbidden and redirected to the same generic error page. This prevents an attacker from distinguishing between “resource does not exist” and “resource exists but requires authentication” — the visible result is identical in both cases.
Security Logging
- Security-relevant events are logged with IP address, browser information, and timestamp. Distinct severity levels (INFO, WARN, ERROR, CRITICAL, AUDIT, SECURITY) and category tags (Login, LoginFailed, AccountLocked, PasswordChange, PasswordReset, SecurityViolation, AccessDenied, Suspicious, Delete, Update, Exception, PageVisit, ClientData) make it possible to filter the audit trail to a specific event class.
- When you visit key pages, your IP address, browser, device type, OS, screen resolution, language, and timezone are recorded. This information is used to help identify and respond to unauthorized access attempts and to reconstruct incidents from the audit trail.
- Every log entry is enriched with structured fields parsed from the User-Agent — device type (mobile, tablet, desktop), browser family, and OS family/version — so the audit trail is filterable without requiring full-text User-Agent searches.
- Every log entry is timestamped with the session ID, allowing the activity of a single session to be reconstructed even when the session has since been abandoned or expired.
- All logging operations are wrapped in exception handlers — a failure to write a log entry cannot crash or degrade the application. The user’s operation continues even if the audit-trail database is briefly unavailable.
- Log field values are individually truncated to their database column limits before insertion (e.g. IPAddress 45 chars, UserAgent 500 chars, PageURL 1000 chars, Message 2000 chars, and the free-text Details field at 32,000 chars). A pathological exception with a giant stack trace, or an oversized User-Agent, cannot fail the log write mid-statement, bloat a single row, or be used as a log-injection or buffer-style attack vector.
- Audit-log writes are queued onto a background work item (
HostingEnvironment.QueueBackgroundWorkItem) rather than executed inline, so the user-visible request is never held up by the log INSERT. The parameter values are fully materialised on the request thread before hand-off, so the background write never touches request state, and the runtime keeps the worker alive for in-flight log items across a graceful application restart. If the host is shutting down and cannot accept the work item, the write falls back to synchronous execution.
- Contact identifiers that would otherwise be written verbatim into a log row are partially masked first (an email becomes
a***@example.com, a phone becomes ***4567). The audit trail can still describe what was attempted without storing recoverable personal data where anyone with database read access could see it; the numeric user ID already on the same row is sufficient for follow-up.
- Environment-capture log writes from anonymous visitors (on the home, join, and login pages) are themselves rate-limited per IP address, so an automated client repeatedly hitting those pages cannot flood the audit table with capture rows. Authenticated capture at sign-up always logs, since it is tied to a real, rare user milestone.
Log Entry Correlation
- Every log entry can be assigned a unique CorrelationID (a randomly generated GUID) that links all log entries from a single request together. The complete sequence of events for any incident is reconstructable even when multiple log entries are written within a single page load and even when log writes are interleaved with concurrent traffic from other users.
Logout & Session Termination
- Logging out is a multi-step process:
FormsAuthentication.SignOut() revokes the authentication cookie, Session.Clear() removes all session data, and Session.Abandon() marks the server-side session for immediate destruction.
- In addition to the framework calls, both the session cookie (PLPSESSION) and the authentication cookie (.PLPAUTH) are explicitly overwritten with empty values carrying a past expiration date. This belt-and-suspenders approach ensures client-side removal even on browsers that do not reliably honor cookie deletion via
SignOut() or Abandon() alone.
- The logout response itself carries
Cache-Control: no-cache, no-store and a past Expires header, preventing the logged-out page from being cached.
- The logout event is logged — including user ID and IP address — before the session is cleared, ensuring the audit trail is never lost.
Return URL & Redirect Safety
- After login, the application redirects you only to a page within Packlist PRO. The redirect target is run through a dedicated whitelist check first: the path portion (everything before any
? query string or # fragment) must end in .aspx, and the value is rejected outright if it contains any whitespace or control character anywhere (closing the " //evil.com" leading-space trick that browsers normalise into a protocol-relative jump), if it begins with //, \\, /\, or \/, if it contains a backslash anywhere, or if it carries a URL scheme (a : appearing before the first /, ?, or # — the shape of http:, javascript:, or data:). A query string and fragment are deliberately preserved so a legitimate ?tripID=… survives the round-trip.
- The “last page visited” stored in the database for post-login redirect is subject to the same validation rules as a query-string return URL. Values from the database are not trusted without passing the whitelist check.
- All post-authentication redirects target relative paths within the application. It is not possible to redirect a user to an external domain through the login flow.
Previous Page Tracking Safety
- The referring page is stored in the session to enable Back button navigation. Before storing it, the referrer host is compared to the current request host — only same-origin referrers are recorded. External referrers are silently discarded, preventing navigation state from being influenced by attacker-controlled external pages.
- When a “Back” link is rendered from that stored value, a second guard (
BasePage.ResolveBackUrl) accepts the target only if it is a local path (beginning with / or ~/) and is not the page you are already on; anything else falls back to the home page. An absolute or off-site URL can never become the destination of an in-page Back link — important for installed-PWA users who have no browser Back button.
Outbound Email Security
- All outbound email is transmitted over an encrypted SMTP connection using TLS on port 587. The
enableSsl setting in Web.config mandates encryption — plaintext delivery is not possible.
- SMTP server credentials are read from
Web.config at runtime via the framework’s system.net/mailSettings section. The SmtpClient instances used in application code do not contain hardcoded credentials.
- The SMTP client timeout is explicitly capped at 15 seconds (overriding the framework default of 100 seconds). A slow or unreachable mail server cannot pin a request thread for an extended period, and an error-page render cannot stall behind a hung outbound notification.
- Every email subject is passed through a header-sanitization step that strips carriage-return and line-feed characters and clamps the length before the message is built. A user-supplied value that flows into a subject line (for example a trip name) cannot inject additional SMTP headers.
- Email send operations are wrapped in their own exception handlers. A mail-server outage cannot prevent the underlying user action from completing or the error page from being shown to the user.
- Notification-class emails are sent on a background work item so the user’s request returns immediately; if a background send fails, the failure is logged with the recipient and message body deliberately omitted (bodies can carry one-time links and recipients are personal data) — only a fixed-format subject is recorded.
- The locked-account alert email passes the offending username through
HttpUtility.HtmlEncode before embedding it in the message body. An attacker who submits a malformed or markup-laden username cannot inject HTML into the alert.
- Outbound email message and SMTP client objects are wrapped in
Using blocks so the underlying network socket is closed immediately after the send completes, even if an exception bubbles up from the SMTP transport.
Output Encoding Discipline
- Every user-controllable value that is rendered into HTML — trip name, note title and body, location and address fields, tag labels, user first name, the IP address shown on the login page, error messages echoed back from validation, and feedback type values — passes through
HttpUtility.HtmlEncode() immediately before being written to the response.
- Encoding is applied at the render site, not at insertion time. Even if a value was inserted into the database before encoding was enforced at that location (for example by a prior version of the code, an import script, or a developer using SSMS directly), the page rendering it still encodes the value safely.
- Hand-built HTML fragments (trip list items, tag accordion buttons, ContactUs URL detection, AI metadata) explicitly call
HttpUtility.HtmlEncode on every user-controllable substring before concatenation. There is no code path that writes a raw user value into rendered HTML.
Built-in Provider Isolation
- The legacy ASP.NET built-in Membership and Role providers have been explicitly removed via
<clear /> directives in Web.config. No application code or framework component can accidentally invoke default provider implementations, which carry weaker security assumptions and are not compatible with this application’s custom authentication model.
- The ASP.NET RoleManager is disabled entirely (
enabled="false"). Role decisions are made exclusively by application code reading from session data and the database — the framework’s built-in role infrastructure plays no part.
Database Connection Management
- The database connection pool is capped at 500 connections (
Max Pool Size=500 in the connection string). This places a hard upper bound on simultaneous database connections and prevents a traffic burst from exhausting the SQL Server connection limit.
- All database commands and connections are opened and closed inside
Using blocks, guaranteeing that connections are returned to the pool immediately after use — even if an exception occurs mid-query.
- Result-set reads are performed through a
SqlDataAdapter that fills a disconnected DataTable or DataRow and then immediately releases the underlying connection back to the pool. The application never holds an open server-side cursor across application code, so a slow or stalled caller cannot pin a database connection.
- The connection string sets
TransparentNetworkIPResolution=False, preventing the .NET SQL client from performing additional DNS resolution attempts that could add latency or fail silently in some network environments.
- The connection string is read once at class-load time from the
ConnectionString entry in <connectionStrings>. There is no code path that constructs a connection string from concatenated input, so a malicious value cannot influence which database, server, or credentials are used.
- Every command (
ExecuteNonQuery, ExecuteScalar, ExecuteDataTable, ExecuteDataRow) exposes a ParamArray of SqlParameter objects. Calling code constructs queries with @Name placeholders and binds values through parameters; there is no overload that accepts string interpolation, so a developer cannot accidentally introduce a concatenated query.
- Nullable values are routed through dedicated helpers (
Param, ParamNullableInt, ParamNullableDate) that explicitly convert Nothing to DBNull.Value. A null value cannot become an empty string or a literal “Nothing” in a generated SQL statement.
- Reader and row accessors (
GetInt, GetString, GetBool, GetNullableDate) check for DBNull and missing columns before reading. A schema change that drops a column does not throw inside the access layer — it returns the documented default, preventing a partial deployment from cascading into a runtime exception.
CSRF Protection for AJAX, WebMethods, and Handlers
- Every authenticated request issued by the page receives a per-session CSRF token, generated as a cryptographically random 32-character GUID and stored only in server-side session memory. The token is exposed to client JavaScript through a single inline assignment (
window.plpCsrfToken) injected by the base page at render time; it is never written to a cookie, never embedded in a URL, and never reflected into the DOM as an attribute.
- The shared AJAX helper (
PLP.callMethod) reads window.plpCsrfToken on every call and forwards it as a custom X-PLP-CSRF request header. Every state-changing WebMethod (trip save, trip permissions, trip notes, trip delete, trip select, trip duplicate, note email, etc.) and every HTTP handler (NoteService.ashx) invokes SecurityHelper.ValidateCsrfToken() as its first action — before authentication is even checked, before any database query runs.
- Token comparison is performed in constant time. The validator XORs each character of the submitted header against the stored session token and accumulates the bit-difference, returning equality only after walking the full string — never short-circuiting on the first mismatched character. This prevents the timing side-channel that a length- or byte-position-sensitive comparison would expose to a network-adjacent attacker.
- Cross-origin AJAX delivery of the token is impossible: the
X-PLP-CSRF header is not a CORS-safelisted header, so any cross-origin fetch or XMLHttpRequest that attempts to set it triggers a CORS preflight. The server never answers that preflight with an Access-Control-Allow-Origin grant, so the browser blocks the request before the actual call is ever sent.
- When a CSRF token goes stale — for example after a server restart recycles the session while a tab is still open — the server tags the rejected response with an X-PLP-CSRF-Status: expired header. The shared AJAX helper detects the tag, transparently fetches a fresh token, and replays the original call exactly once. A token that simply aged out is recovered without the user ever seeing a spurious “invalid request” error, and the replay is hard-capped at a single attempt so a genuinely forged request cannot loop.
- The token-refresh endpoint is the one component deliberately exempt from CSRF validation — requiring the token in order to fetch the token would deadlock the recovery path — but it is otherwise tightly bounded: it requires an authenticated session, is
POST-only so the token never lands in a URL or referrer, returns the token only to same-origin script that the browser already gates, and is rate-limited to 20 requests per user per minute, with a breach of that limit logged as a security violation.
- Every CSRF validation failure — whether the header is missing, the session token is missing, or the values do not match — is recorded as a
SecurityViolation event with the requesting user ID and the request path, so probing attempts surface in the audit trail even when the endpoint silently returns “Invalid request.”
- The token comparison logic is itself wrapped in a try/catch that swallows any exception from the logging path. A logging-layer outage cannot crash the CSRF validator and cannot leak via an unhandled exception.
- Login and Contact Us forms each maintain their own page-scoped CSRF token, regenerated on every form load and rotated again immediately after each submission. A captured form post cannot be replayed even within the same session, even within the same minute.
Cryptographic Discipline
- Every per-session secret — the AJAX CSRF token, the per-form login CSRF token, the per-form contact-us CSRF token, the trip sharing TripLinkCode, and the per-request CorrelationID where used — is generated from
Guid.NewGuid(), which under .NET on Windows draws from the OS cryptographic random source (BCryptGenRandom). No security-relevant identifier is derived from a counter, timestamp, or predictable seed.
- Temporary passwords issued by the account-recovery flow are generated with a cryptographic random number generator (
System.Security.Cryptography.RandomNumberGenerator), never the non-cryptographic System.Random. The alphabet deliberately omits the visually ambiguous glyphs 0/O and 1/l/I so the value can be transcribed from an email or text message without confusion, and a minimum length is enforced in code regardless of the requested length.
- Internal cache keys for cached AI responses use SHA-256 over the model name and full prompt text — and, for answers whose correctness depends on the date, the current UTC date is folded into the hashed input so a date-sensitive response is never served stale from a prior day. The 256-bit hash space prevents prefix-collision games and means a malformed prompt cannot collide with a legitimate cached response.
- Writes into the AI-response cache are guarded by a
WHERE NOT EXISTS check on the cache key, so two requests computing the same answer at the same moment cannot insert duplicate or interleaved rows for a single key.
- ViewState integrity uses the framework’s default HMAC algorithm with the machine key. No application code attempts to roll its own MAC or hash for ViewState; the framework primitive is used exactly as designed.
- The framework’s cryptographic keys are explicitly pinned in configuration (
<machineKey> with validation="HMACSHA256" and decryption="AES") rather than being auto-generated per worker. Pinned keys mean a MAC-signed ViewState payload, an encrypted Forms Authentication ticket, or a protected cookie stays valid across application-pool recycles and is verified identically by every worker process — there is no key-rotation window for an attacker to exploit, and because the keys are unique to this site a ticket minted here is never accepted by any sister application.
Configuration Whitelisting
- Every user-selectable preference value is validated server-side against an explicit whitelist before persistence:
ColorScheme must be exactly “L” or “D”; SiteMode must be 1, 2, or 3; DetailLevel must be 1, 2, or 3; FontSize must be 14, 16, or 18. Any other value falls back to a safe default rather than being written through — a tampered dropdown post cannot smuggle an out-of-range value past the database CHECK constraints.
- The Contact Us feedback category is validated against a fixed string list (
bug, quality, enhance, feature, security, access, support, abuse, other). Tampered or unexpected values are rejected before the database insert or the outbound email send.
- The pre-select query-string parameter that lets a deep-link to Contact Us pre-fill the feedback category is validated against the same whitelist before being applied to the dropdown. A crafted URL cannot inject an arbitrary value into the rendered form.
- Where comma-separated UserID lists appear in application settings, each token is parsed as a positive integer through
Integer.TryParse. Non-integer tokens, negative values, and the literal string zero are silently dropped so a typo in the configuration file cannot accidentally widen the set.
Time Handling
- All timestamps written to the database (note created, note updated, trip created, trip updated, login, logout, lockout, log entry, AI cache entry, file upload) are stored as UTC via
GETUTCDATE(). The audit trail and concurrency tokens are unaffected by daylight-saving transitions or server time-zone changes.
- UTC values are converted to the user’s local time zone only at render time, using a
TimeZoneInfo.Id the user explicitly set on their profile. Conversion is performed inside a try/catch — if the stored time-zone ID is unrecognised (for example, when the underlying OS time-zone database has been updated), the display falls back to server-local time rather than throwing a rendering exception.
- Time-zone lookups are routed through a process-wide cache that records both successful and failed resolutions. A malformed or unknown stored time-zone ID is resolved once, its negative result is cached, and every later row carrying the same bad ID skips both the registry lookup and the exception — so a single corrupt profile value cannot amplify into a storm of repeated throws across a long list render or the reminder batch.