⚠ You are offline. Changes cannot be saved until your connection is restored.
Site-Wide Security
Data Protection
- Database queries use parameterized statements (
SqlParameter bindings through a centralized DatabaseHelper), which prevents SQL injection attacks.
- User-provided content is HTML-encoded at render time before being displayed, preventing cross-site scripting (XSS) attacks.
- Server-side validation is performed on all inputs independently of any client-side validation.
- Query-string IDs are parsed through
BasePage.TryGetQueryStringInt() — non-numeric or negative values fall back to a safe default before being concatenated into any SQL or used to load a record.
- Dropdown values posted back from data-management screens are cast to integers before being used in queries — defense-in-depth on top of EventValidation.
Authentication & Session Management
- Authentication uses an encrypted and cryptographically signed Forms Authentication ticket (
protection="All"): the cookie payload is both AES-encrypted (its contents cannot be inspected) and HMAC-signed (a single-byte modification invalidates the entire ticket).
- If your session data is lost (for example, after the server’s application pool recycles), it is automatically rebuilt from your valid authentication ticket in
Global.asax.Application_AcquireRequestState — you remain logged in without re-entering your credentials. The rebuild runs before the page lifecycle starts, so every request type (pages, AJAX page methods, .ashx handlers) benefits from the same recovery.
- Sessions use non-default cookie names (
QFITSESSION for the session ID, qFITV2AUTH for the auth ticket) 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.
- When the session is rebuilt from a ticket, the account’s
Active and Approved flags are re-checked against the database. A revoked account is signed out at that moment regardless of how long the ticket is still valid for.
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.
- State-changing handlers (save/delete buttons) call
BasePage.VerifyAuthenticatedOrShow() as a defense-in-depth re-check — if the session has expired mid-edit, the user sees a clear “session expired” message without losing their form input.
- Per-record ownership is enforced at the database stored-procedure layer: the proc requires a UserID argument and filters every UPDATE or DELETE on that ID, so a bug in application-layer code could not cause one user’s record to be modified by another.
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, payment, USB, magnetometer, gyroscope, accelerometer, interest-cohort, browsing-topics). 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 qFITpro.com 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 (including X-Forwarded-Proto from the Plesk SSL terminator), 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 altogether.
- Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Resource-Policy: same-origin isolate the document from cross-origin window references and prevent its resources from being embedded by other origins.
- The
X-Powered-By, X-AspNet-Version, and Server 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.
- The keys that sign and encrypt ViewState (and the Forms Authentication ticket) are explicitly pinned in configuration (
<machineKey> with validation="HMACSHA256" and decryption="AES") rather than being auto-generated per worker. Pinned keys mean a signed or encrypted payload stays valid across application-pool recycles and is verified identically by every worker process, with no key-rotation window for an attacker to exploit; the keys are unique to qFIT, so a ticket minted here is never honoured by a sister application.
- 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.
- 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.
- 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"). 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 where attacker-controlled data (a redirect target, a cookie value) would otherwise inject a second header or a forged response body.
Cookie & Token Handling
- Authentication tokens and session IDs are stored in cookies only (
cookieless="UseCookies") — never placed in URLs. 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 carries the UserID 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 (
QFITSESSION) and the authentication cookie (qFITV2AUTH) 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.
- 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 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, forgot-password, invite sending, account sign-up (Join), 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.
- Throttle thresholds and time windows are configurable per operation in
Web.config (ThrottleLogin_*, ThrottlePasswordReset_*, ThrottleInvite_*, ThrottleContactUs_*, ThrottleJoin_*) — the limits can be tightened without a code deployment if abuse patterns shift.
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 (with the leftmost entry taken when the value is a comma-separated chain), enabling accurate client attribution when traffic passes through the Plesk SSL terminator or any other reverse proxy. The direct connection address is used as a fallback when no forwarding header is present.
Search Engine & Crawler Controls
- Sensitive pages — including login, account, preferences, and per-user content pages — carry
noindex, nofollow directives. Search engines cannot index these pages or follow their links.
- A
robots.txt file explicitly steers search-engine crawlers away from account-related pages, from the framework-internal directories (/App_Code/, /App_Data/, /App_Start/, /bin/, /obj/, /TEMP/, /packages/), and from the user-upload folder root (/FeedbackFiles/) — so a crawler cannot enumerate uploaded attachments even though an individual file URL remains reachable for legitimate use.
Error Handling
- End users see a friendly error page with no internal stack content; raw technical detail (stack traces, SQL text, file paths) is never exposed to ordinary users.
- Every unhandled exception is logged to the central
plp_Logs audit table with full request context — the request URL, HTTP method, client IP, session ID, and the complete exception chain (up to five levels of inner exceptions) — so failures can be investigated from the log without surfacing any detail to the user.
- Every exception is also appended to a size-capped, self-rotating diagnostic flat-file (
App_Data/last_errors.txt, ~5 MB) that captures crashes occurring after the response has begun flushing — the point at which the normal database-log-plus-error-page path can no longer be shown. Rotation is a single atomic move to a .old sibling (replacing any previous .old), so the total on-disk error data stays bounded and there is no partial-truncation window. The file lives under App_Data, which IIS never serves over HTTP.
- Every 500-level error generates a short 8-character correlation ID that is shown to the user on
Error.aspx and logged into the central plp_Logs.CorrelationID column. A user can quote the ID in a support request and the operator can grep the audit log directly to find the matching entry.
- 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.
- A loop-breaker in
Application_Error detects exceptions that occur while rendering Error.aspx itself and short-circuits the redirect, preventing an infinite 302 chain on a misconfigured production environment.
- A postback whose ViewState fails validation — typically a form submitted across a deployment or after the session was renewed — is recovered gracefully rather than surfaced as a server error: the request is logged, the stale submission is discarded, and the page reloads with a brief “form refreshed” notice. A signed-but-unreplayable ViewState never produces an error page.
Security Logging
- Security-relevant events are logged to the shared
plp_Logs table (with App = "qFIT" so the cross-app log can be filtered down) along with IP address, browser information, and timestamp. Distinct severity levels (INFO, WARN, ERROR, SECURITY, AUDIT) and category tags (Login, LoginFailed, Logout, PasswordChange, PasswordReset, SecurityViolation, AccessDenied, Exception, PreferencesSaved, AccountCreated) 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 time zone 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.
- 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.
- User identifiers (usernames, emails, phones) are masked before being written to the log — an email becomes
j***@gmail.com, a phone becomes ***6789. Enough signal remains to correlate repeated attempts or attack patterns, but the raw value is never persisted into the log table in clear text.
- The audit-log
INSERT is queued onto a background work item, so the user-visible request is never held up by the write; the HttpContext-dependent fields are captured synchronously on the request thread before the work is queued.
Maintenance Mode Gate
- The site can be taken offline for maintenance by flipping a single Web.config flag. When enabled,
Global.asax.Application_PostAuthenticateRequest redirects every request to a dedicated Maintenance.aspx page that returns HTTP 503 Service Unavailable with a Retry-After hint. The gate covers every request type — pages, AJAX page methods, and HTTP handlers — not just pages that use the master.
- Static asset paths (
/Content/, /Scripts/, /Images/, /Shared/, favicon, manifest, service worker, offline page) are bypassed so the maintenance page itself renders with its CSS and icons.
Logout & Session Termination
- Logging out is a multi-step process:
FormsAuthentication.SignOut() revokes the authentication cookie, the Remember-Me token row in dbo.LoginTokens is deleted, Session.Clear() removes all session data, and Session.Abandon() marks the server-side session for immediate destruction.
- The auth cookie (
qFITV2AUTH) and session cookie (QFITSESSION) 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 event is logged — including UserID 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 (carried in the
go query-string parameter). Redirect targets are constrained to relative paths within the application — the login flow cannot be tricked into bouncing the user to an external domain.
- Back buttons resolve through
BasePage.ResolveBackUrl(), which refuses any value that does not start with a single / (rejecting protocol-relative // open-redirect prefixes) and does not contain a host portion.
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 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.
- Every email subject is passed through a header-sanitization step that strips carriage-return and line-feed characters before the message is built, so a user-supplied value flowing into a subject line cannot inject additional SMTP headers.
- Notification-class emails are queued on a background work item so the user’s request returns immediately; an SMTP error on that background thread is swallowed and never surfaces to the user or blocks the action that triggered it. The swallowed failure is still logged as a warning with the recipient redacted to its domain only (
***@domain), so delivery problems can be diagnosed without persisting a recoverable third-party address in the log.
- 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.
Output Encoding Discipline
- Every user-controllable value that is rendered into HTML — usernames, Method/Target/Category names, notes fields on detail pages, the IP address shown on the login page, error messages echoed back from validation — 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 an older 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 (Checklist rows, change-log tables, contact-us submission lists) explicitly call
HttpUtility.HtmlEncode on every user-controllable substring before concatenation.
Built-in Provider Isolation
- The ASP.NET RoleManager is disabled entirely (
roleManager enabled="false"). Role decisions are made exclusively by application code reading from session data and the dbo.Logins.UserRole column — the framework’s built-in role infrastructure plays no part.
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.qfitCsrfToken) injected by BasePage.OnPreRender; 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 (
qFIT.callMethod) reads window.qfitCsrfToken on every call and forwards it as a custom X-QFIT-CSRF request header. A Sys.Net.WebRequestManager hook also injects the same header on every classic PageMethods.* call, so the entire AJAX surface area is covered without per-page changes.
- Every state-changing WebMethod (
Checklist.SetCompleted, ToDo.SetCompleted, ContactUsStatus.GetRecord, etc.) invokes SecurityHelper.ValidateCsrfToken() as its first action — before any database query runs.
- Token comparison is performed in constant time. The validator accumulates the per-character bit-difference over the full string length, never short-circuiting on the first mismatched character. This prevents the timing side-channel that a length- or byte-position-sensitive comparison would expose.
- Cross-origin AJAX delivery of the token is impossible: the
X-QFIT-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 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 UserID and the request path.
- When a token goes stale (after a deploy or an app-pool recycle with the tab still open), the server tags the rejected response X-QFIT-CSRF-Status: expired and the client transparently fetches a fresh token from a dedicated refresh handler and replays the call once, so a benign stale token never surfaces an error. That refresh handler returns HTTP 401 for an unauthenticated or expired session and HTTP 429 once a single user exceeds twenty refreshes per minute (recording the attempt), so the recovery path is authentication-gated, same-origin-only, and hard-throttled against any attempt to enumerate or brute-force tokens.
Configuration Whitelisting
- User-selectable preference values are validated server-side against an explicit allowlist before persistence:
ColorScheme must be L or D; SiteMode 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.
- The Contact Us feedback category is constrained to a fixed dropdown (
bug, quality, enhancement, feature, security, access, support, abuse, other); server-side EventValidation rejects any value not present in the rendered control before the database insert.
- Comma-separated UserID lists in
Web.config parse each token through Integer.TryParse. Non-integer tokens, negative values, and zero are silently dropped so a typo cannot accidentally widen the set.
Remote Sign-Out (Sign Out Everywhere)
- Each signed-in device carries a per-device security-stamp cookie (
qfit_ss, HttpOnly). On every session rebuild the device’s stamp is compared against the account’s current stored stamp; if you rotated the stamp from another device, this device’s still-valid ticket stops matching and is signed out on its next request. This is what lets a single “sign out everywhere” action terminate every other logged-in session even though the authentication ticket itself is long-lived.
- The check is fail-open by construction — it only ever rejects when both a device cookie and a stored stamp are present and differ — so a missing or not-yet-issued stamp can never force an unexpected logout.
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. The threshold and window are configurable in
Web.config — they are not hardcoded.
- The rate-limit counter is stored in server-side memory — it cannot be reset by clearing browser cookies, switching browsers, or opening a new session.
Account Lockout
- In addition to the in-memory IP throttle, qFIT tracks failed login attempts in a persistent
FailedAttempts / LockedUntil pair on the account row. Once the failure count crosses the configured threshold the account is locked for the throttle window, and a locked account is refused even when the correct password is later supplied, until the lock expires. Because the counter lives in the database, the lockout survives an application-pool recycle — an attacker cannot reset it by waiting out a restart.
- After an incorrect password on an existing account, the page shows how many attempts remain before lockout, while a non-existent username still receives the identical generic failure message — so the countdown never reveals whether a username exists.
- The failed-attempt counter is incremented with a single atomic database statement (
UPDATE … OUTPUT INSERTED.FailedAttempts), so two simultaneous failed attempts cannot each read a stale count and slip past the lockout threshold — the database serialises the increment. The same statement stamps a LastFailedAttemptAt timestamp on the account row, giving a forensic timeline directly on the account independent of the central log table.
Credential-Stuffing Detection
- qFIT counts failed logins originating from a single IP within the throttle window; when an IP crosses the configurable
ThrottleLoginStuffing_Threshold, a security event is logged and a one-time alert is sent to the operator. The alert is de-duplicated with a per-IP flag so a sustained attack generates one notification per window rather than a flood, and the whole routine is wrapped so a mail-server outage can never block a legitimate login. This credential-stuffing alert is the only email a login event ever generates.
Credential Protection
- Failed login attempts show the same error message regardless of whether the username or the password was incorrect. This prevents attackers from discovering which usernames exist on the platform.
- Password comparison is performed in constant time. The comparison routine accumulates the per-character bit-difference over the full string length and never short-circuits on the first mismatched character, removing the timing side-channel that a position-sensitive comparison would expose.
- The password field is never pre-populated from any stored source. The previous “u”/“p” plaintext-cookie auto-submit path was deliberately removed as part of the May 2026 security pass.
- After login, you are only redirected to relative paths within qFIT — open-redirect attacks through the login flow are not possible.
- After a successful login, the account’s
Active and Approved flags are checked against the database. An account with a correct password is still rejected if either flag is unset, so clearing either flag immediately blocks subsequent logins.
- Password fields are not trimmed before comparison — a password that begins or ends with a space is verified exactly as entered.
Session Rotation on Login
- After a successful login, the pre-login session is abandoned via
Session.Abandon() and its cookie is overwritten with an empty value bearing a past expiration date. The browser receives a fresh QFITSESSION identifier on the next request, so any pre-authentication session ID known to an attacker (for example, planted by a session-fixation link) cannot be promoted to an authenticated session.
- The Forms Authentication ticket carries the UserID only and is set with
protection="All" (encrypted + HMAC-signed). The bearer cookie cannot be inspected or modified by the client.
Remember-Me Token
- The optional “remember me” cookie (
qFIT_Remember) carries a random 64-character token. The token is hashed with SHA-256 before storage in dbo.LoginTokens — the database stores only the hash, so a database snapshot does not expose any usable bearer credential.
- Each token is single-purpose: it lets a returning browser silently re-establish an authenticated session, but never bypasses the password check on the human-typed login form. Its expiry is extended (slid forward) on each silent use, a fresh token is issued on each interactive login, and the underlying row is deleted on explicit logout — so a stolen device’s persistent login is revoked the next time the legitimate user signs out.
- Token rows carry an explicit
ExpiresAt column and are rejected past expiry, and the silent auto-login path re-checks the joined account is still Active and Approved before establishing a session — an expired token or a revoked account matches nothing. An abandoned device’s Remember-Me token cannot be replayed indefinitely.
- When a password is reset, every Remember-Me row for that account is deleted, forcing all silent-login devices to re-authenticate with the new password.
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 time zone are recorded when the login page loads.
- Successful login resets the IP-based throttle counter, 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 so modern password managers can 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.
Account Recovery & Sign-Up
Account Recovery
- The account-recovery flows (forgot username, forgot password) accept an email address or phone number and respond with the identical “if an account exists, you’ll receive…” message on every exit path — no match, no contact on file, or profile missing — so the form cannot be used to probe which addresses are registered.
- Both flows are throttled simultaneously by IP and by the submitted identifier; a throttled request is logged as a security violation with the identifier masked.
- If the submitted email or phone matches more than one account, the request is refused rather than acting on an arbitrary match (which could otherwise leak the lowest-ID account’s credential to whoever owns a shared contact), and the same vague confirmation is still returned.
- A reset password is generated with a cryptographic random number generator (not a clock-seeded PRNG), using rejection sampling to avoid modulo bias and an alphabet that excludes look-alike characters (0/O, 1/l/I) so the emailed temporary password is unambiguous.
Sign-Up
- Final account creation is throttled by IP (
ThrottleJoin_*); a throttled attempt is logged as a security violation and rejected.
- A server-side password policy is enforced independently of any client-side check: a minimum length, at least three of the four character classes (upper / lower / digit / symbol), a minimum number of distinct characters, no leading or trailing space, no common weak substrings, and the password may not contain or equal the username.
- Usernames are validated server-side to a length range, an allow-listed character set, a minimum distinct-character count, and a ban on common weak substrings.
- Every prior-step value (name, phone, email, city, postal code) is re-validated server-side for length, allowed-character pattern, and profanity before the account row is written, so a bot posting past the HTML field limits cannot push an over-length or malformed value into the database.
- Sign-up refuses to create a second account when the submitted email or phone is already on file, directing the user to log in instead. The account, profile, and contact rows are all written through parameterized statements with width-truncated values and the server-issued UserID — never a client-supplied identity — and a duplicate-username race is caught and surfaced as a clean “already in use” message rather than a raw database error.
- On successful account creation, any pre-existing “remember me” token presented by the browser is deleted from
dbo.LoginTokens and every request cookie is expired, so a stale persistent login left behind on a shared browser cannot carry over onto the brand-new account.
Per-Record Ownership (Checklist, Categories, Targets, Methods)
Stored-Procedure Ownership Gate
- Every save and delete on a Category, Target, or Method routes through a stored procedure (
df_qfit.UpdateqFITGroup, df_qfit.DeleteqFITGroup, and equivalents for Targets and Methods) that requires a UserID argument and includes it in the WHERE clause. Passing another user’s record ID simply matches zero rows — the database does the access-control enforcement, independent of any application-layer check.
- The application layer also enforces the same rule before issuing the call: query-string IDs are validated through
BasePage.TryGetQueryStringInt() and the current UserID is taken from the rebuilt Session rather than from any user-supplied source.
Detail Pages
- The
CategoryDetails, TargetDetails, and MethodDetails reads validate their query-string IDs and bind through SqlParameter — a non-numeric or negative ID never reaches the database.
- The “Save Notes” handlers on each Detail page (historically tagged with a SQL-injection TODO) were migrated to parameterized statements via
DatabaseHelper.ExecuteNonQuery with SqlParameter bindings.
Checklist State Changes
- The
Checklist.SetCompleted and ToDo.SetCompleted WebMethods validate the X-QFIT-CSRF header before doing any work, then verify the calling user owns the Method whose state is being toggled. A check-off on someone else’s Method is rejected at the data layer.
Write Rate-Limiting & Input Validation
- Both
SetCompleted WebMethods apply a per-user write throttle under a shared key, defaulting to 300 writes per minute (configurable via the ThrottleSetCompleted_MaxAttempts / ThrottleSetCompleted_WindowMinutes settings) — generous for a fast check-off session but capping a malicious script at roughly five writes per second, even one alternating between the Checklist and To-Do pages. A throttled call is logged as a security violation.
- Each call rejects any record
type outside the expected set (G/I/E) and any non-positive id before touching the database, and every INSERT/DELETE is filtered on WHERE UserID = @UserID, so a check-off only ever affects a row the caller owns.
- Every page-method on these pages — not only the state-changing check-off, but also the read methods that load data — validates the X-QFIT-CSRF token as its first action and returns an empty result on failure.
Record-Level Access Guards
- The Category, Target, and Method detail reads filter every load on “owned by you, or the shared public template” (
(UserID = template AND IsPublic = 1) OR UserID = @UserID). A record owned by another user renders a “record does not exist” message with the action controls hidden — the same response whether the ID is unowned or genuinely absent, so record IDs cannot be probed.
- The edit pages apply the same owner-or-public-template filter when binding the form, not just on save, so another user’s private record’s name and settings cannot even be viewed by tampering with the ID. The save and delete handlers re-validate the ID and re-check authentication before doing any work, so a session that expired mid-edit is caught without losing input.
Account & Session Security
Session Rebuild After Interruption
- ASP.NET InProc sessions are lost if the server’s application pool restarts. When this happens,
Global.asax.Application_AcquireRequestState detects the missing session and reconstructs it from your still-valid Forms Authentication ticket by re-querying your username, role, first name, email, and time zone from dbo.Logins LEFT JOIN dbo.BasicUserProfiles. You remain logged in and your work is not interrupted.
- The rebuild runs before the page lifecycle starts, so it benefits every request type uniformly: pages, AJAX page methods, .ashx handlers, and the master page’s own nav configuration.
- The rebuild only succeeds when a valid, non-expired, cryptographically intact authentication ticket is present and the embedded identity parses as a positive integer matching a known account that is both
Active and Approved. A missing, expired, or tampered ticket results in an anonymous session, not a reconstructed one. A revoked account is signed out at the rebuild step.
- Session rebuilds are logged at INFO level with the UserID 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 (
qfit_theme, qfit_font, qfit_sitemode, qfit_tz) carry only display telemetry — no identity, no authentication token, no permission flag. The site-wide <httpCookies> default ensures HttpOnly=true and SameSite=Lax apply to all of them.
- On a cold login (new device, fresh browser) these preference cookies are hydrated into
Session by the centralized helper, so the theme and font settings apply from the very first render. The hydration only fires when Session does not already carry the value, so a per-session change is never silently reverted to the cookie state.
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 fallback only succeeds when the FormsAuth ticket is present, decryption succeeds, and the embedded identity parses as a positive integer matching a known active and approved user. A missing, expired, or tampered ticket results in an unauthenticated response, not a recovered session.
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 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.
- When the ticket path is taken, the resolver re-checks the account’s
Active and Approved flags against the database and calls FormsAuthentication.SignOut() when the account no longer exists or has been revoked — so a banned or deleted user cannot keep riding a still-valid long-lived ticket through AJAX and handler endpoints, independent of the rebuild-time check. If the database is briefly unreachable the check fails open for that single request only and is re-evaluated on the next one, so a transient outage neither locks everyone out nor grants a lasting bypass.
Anonymous Access Boundaries
- Anonymous users can view a curated set of public pages (About, Help, Tips, Walkthrough, Tutorial, Contact Us, etc.). Every other page is gated by the master page’s
PublicPages allowlist and the per-page BasePage.RequireAuthentication() check, plus an explicit <deny users="?" /> in Web.config for the most sensitive ones (such as Preferences and ContactUsStatus).
- The invite flow (
Invite.aspx) is open to anonymous users so word-of-mouth invites work without forcing the inviter to sign up first, but the per-IP rate limit applies regardless of authentication status, and a per-UserID limit stacks on top for authenticated users.
Preferences Persistence
- Preferences (theme, font size, site mode, time zone) save to both
Session and a long-lived cookie. The cookie acts as the durable cross-device source so the theme survives a fresh login on a new device, while the session value is the per-request authority. A tampered cookie value falls back to a safe default rather than propagating an out-of-range value into the rendered page.
Re-Authentication for Credential Changes
- Changing your username or password requires re-entering your current password, even though you are already signed in. This step-up check stops someone at a walked-away, still-logged-in browser from silently taking over the account by changing its credentials. An incorrect current password is rejected, recorded as a security violation, and counted against a dedicated per-user throttle (10 attempts in 15 minutes), so the current-password prompt cannot be brute-forced across repeated postbacks.
- The same current-password re-authentication gate also guards self-service account deactivation, so a hijacked open session cannot deactivate the account either.
Self-Service Account Deactivation
- Deactivating your account is a reversible soft delete: it sets the account’s
Active flag off on your own row only (after the current-password re-authentication and an explicit confirmation tick) and preserves your data so it can be reactivated — nothing is hard-deleted. The UPDATE is parameterized and scoped to your own UserID.
- Deactivation also deletes every “remember me” token for the account and signs out the current device. Because the
Active flag is re-checked on every login, every session rebuild, and every AJAX identity resolution, a deactivated account is locked out of all devices immediately — a still-valid long-lived ticket on another device stops working on its very next request.
Sign Out Everywhere
- The “sign out everywhere” control rotates your account’s server-side security stamp (which makes every other device’s stamp cookie stop matching on its next request — see Remote Sign-Out in Site-Wide Security above) and deletes every “remember me” token so no other device can silently auto-login, then signs out the current device too. It is deliberately not gated behind a password re-prompt: a session-ending action cannot read your data or change your credentials, so by the least-privilege principle it stays available even if you cannot recall your password on the spot.
Contact Form Security
Form Integrity
- The contact form is protected by the framework’s ViewState MAC + EventValidation +
ViewStateUserKey binding. Every submission is verified to originate from a page rendered for the same session — a captured form submission cannot be replayed by a different session, and a postback that fabricates events for hidden controls is rejected before any application code runs.
- Request validation (
validateRequest="true") rejects any submission containing potentially dangerous HTML or script characters at the framework boundary, before btnSubmit_Click ever runs.
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 UserID (or the literal “anon” bucket for unauthenticated submissions). 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 are independently configurable in
Web.config (ThrottleContactUs_MaxAttempts, ThrottleContactUs_WindowMinutes), separately from the login and password-reset throttles.
- Every throttled rejection is logged as a
SecurityViolation event with the offending IP address.
Bot Detection
- The contact form includes a visually-hidden,
aria-hidden, tabindex="-1", autocomplete="off" honeypot field. A submission that fills it is silently dropped, logged as a security violation with only a masked, length-capped preview (never the raw payload), and shown a fabricated “thanks” message so the bot believes it succeeded and does not retry.
Duplicate Submission Defense
- The previous submission text is held in session and compared against any new submission. A duplicate submission within the same session is rejected with a clear message, preventing accidental double-posts (such as a double-click on the Submit button while a slow network is being recovered).
File Attachment Size Limits
- Individual file size is capped at 10 MB. Total combined size across all attachments is capped at 50 MB. Both limits are enforced server-side before any file is written to disk. An oversized submission is rejected with a clear message before reaching the database.
- Uploaded filenames have the
# character stripped (which is illegal in URL paths) and are prefixed with the database-generated FeedbackID before being written to disk. Two submissions cannot collide on disk even if their original filenames were identical, and there is a stable cross-reference between the stored blob and the database record.
Input Validation
- The feedback category dropdown offers a fixed set of values (
bug, quality, enhancement, feature, security, access, support, abuse, other); the submission requires a category to be chosen, and server-side EventValidation rejects any value not present in the originally rendered dropdown before it reaches the database.
- The comment body is capped at 4000 characters by an explicit server-side length check — a multiline textbox’s
MaxLength is not enforced by the browser, which is exactly why the server-side check exists — with the database column enforcing the upper bound independently.
- 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.
My Submissions Visibility Gate
- The
ContactUsStatus page that lets you read your own submissions filters every query on UserID = current, so a user can never load another user’s row even by manipulating the FeedbackID in the request. The detail-loading WebMethod validates the X-QFIT-CSRF token first, resolves the user from session-or-ticket, then filters the row on FeedbackID = @FID AND UserID = @UserID AND App = @App — so you cannot load another user’s row, nor even your own submissions filed under a different application, by tampering with the FeedbackID. The gate runs at both the page load and at every AJAX call to fetch a row.
Progressive Web App Security
Cross-Origin Request Isolation
- The qFIT service worker intercepts requests made by the app. Any request directed at a domain other than qFIT’s own origin is passed through to the network without being handled by the service worker. There is no execution path in which a cross-origin response can enter the service worker’s cache.
- The cross-origin check is performed in the first lines of the fetch handler — before strategy selection, before path matching, before any cache lookup.
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.
API Path Exclusion
- Any request whose path begins with
/api/ is excluded from service worker handling regardless of method or origin. The namespace is intentionally reserved and inaccessible to caching, so a future API call cannot be silently replayed by the worker after an offline period.
Network-First Strategy for Data Pages
- Pages that display your Checklist, To-Do, My Week, and Dashboard 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 Responses Not Cached
- The service worker does not cache AJAX page-method responses. Sensitive JSON responses from
Checklist.SetCompleted, ToDo.SetCompleted, ContactUsStatus.GetRecord, and the rest of the page-method surface are never written to browser storage by the worker.
Static Asset Caching
- Static resources (images, fonts, icons) use a cache-first strategy with a network fallback. CSS and JavaScript files use network-first-with-cache so a fresh deploy lands immediately while still surviving an offline period.
Cache Version Management
- Each service worker deployment carries a unique cache version identifier (
CACHE_VERSION). 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.
- If even the cached offline page is unavailable, the worker still resolves the request with a fully self-contained HTTP 503 page whose markup loads no external CSS, script, or fonts — so the offline state always renders rather than a broken, network-dependent shell.
- On update, the new worker deletes prior-version caches and takes control immediately (
skipWaiting + clients.claim), so a security-relevant worker change cannot sit deferred behind an open tab; navigation preload is enabled so the first navigation is not stalled while the worker boots.
- 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.
Client-Side & AJAX Defenses
Session Expiry Detection
- Every AJAX call made through the shared
qFIT.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 so you land back where you left off) or stay on the page in view-only mode while keeping your in-progress input.
- State-changing save handlers also call
BasePage.VerifyAuthenticatedOrShow() on the server side. If the session expired between page load and form submit, the user sees the “session expired” toast and the form input is preserved — they re-authenticate via the modal and resubmit without losing their work.
Offline-Aware Saves
- Every AJAX call checks the connection state before attempting to send, and that state is not a raw read of the flaky
navigator.onLine flag: an offline event is debounced for three seconds and then confirmed with a HEAD request to /favicon.ico (a tiny, no-store probe) before the connection is treated as down. If the browser really is offline the call is short-circuited locally and the supplied error handler receives an offline message immediately, without firing a doomed request — but a momentary cell-tower flicker no longer produces a spurious “offline” failure.
- An
online/offline event listener toggles a CSS class on document.body and surfaces a banner. There is no scenario in which the user appears connected but their input is being lost.
Save-Button Lifecycle
- The page’s save helpers (
qFIT.setSaving, setSaved, setSaveError) drive an explicit three-state lifecycle for buttons: in-flight (disabled + spinner), saved (success indicator), and error (red flag). 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 from running.
Per-Request CSRF Header Injection
- The shared AJAX helper reads the per-session CSRF token from
window.qfitCsrfToken (injected by BasePage on every render) and sets the X-QFIT-CSRF request header on every call. 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 classic
PageMethods.* call path is also covered: a Sys.Net.WebRequestManager.add_invokingRequest hook injects the same X-QFIT-CSRF header on every PageMethods call automatically, so no page-level wiring is required.
No Sensitive Data in Local Storage
- The application does not write authentication tokens, session identifiers, email addresses, or Checklist content to
localStorage or sessionStorage. Per-page UI preference state (filter toggles, panel open/closed) is the only category of value that lives in browser storage, and each value is validated against a known set before being applied.
Request Timeout
- The shared AJAX helper sets a 30-second request timeout on every call. A hung server, broken pipe, or vanished mobile connection results in a clear error message to the user instead of an indefinitely pending request that consumes a UI slot.
Duplicate-Request Coalescing
- The shared AJAX helper keys every call by its URL plus its serialized request body and coalesces duplicates: a second identical call made while the first is still in flight does not open a second network request — it registers its callbacks against the pending one and is notified when that settles. A rapid double-click, or two components triggering the same save at once, therefore cannot double-fire a state-changing WebMethod at the transport layer.
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) uses 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, the Server header, and the framework version banner (enableVersionHeader="false" in <httpRuntime>) are all suppressed. Automated scanners cannot fingerprint the platform from response headers.
- The
Server banner is additionally stripped at the native IIS request-filtering layer (requestFiltering removeServerHeader="true"), not only in managed code, so the server-identity header is absent even on responses produced before the managed pipeline runs. The same request-filtering block adds Temp to IIS’s hidden-segments list, so the scratch /Temp/ directory returns 404 and cannot be requested over HTTP at all.
Request Size Limits
- Maximum request size is capped at 100 MB at both the ASP.NET (
maxRequestLength) and IIS (requestLimits maxAllowedContentLength) layers — reduced from a legacy 1 GB limit that allowed a single oversized upload to tie up a worker thread and pre-allocate memory — bounding the resource cost of any single request.
Layered Error Routing
- HTTP status codes 401, 403, and 404 are routed to the application’s generic
Error.aspx page via the ASP.NET <customErrors> block. The framework’s global error handler in Global.asax.Application_Error intercepts every unhandled application exception, logs the full context with an 8-character correlation ID, and routes the user to the same generic page.
- The 500 entry is intentionally absent from
<customErrors> so a 500 inside Error.aspx itself doesn’t bounce back through the same redirect rule. The loop-breaker in Application_Error catches the recursive case explicitly and short-circuits.
Static Folder Authorization
- Asset folders (
Content/, Scripts/, Images/) and PWA plumbing (manifest.json, service-worker.js, offline.html) are explicitly opened to anonymous access through scoped <location> elements in Web.config. Authentication-restricted pages inherit the default deny posture by virtue of not appearing on the allowlist, and the most sensitive ones (such as Preferences and ContactUsStatus) carry an explicit <deny users="?" /> as defense in depth.
- Framework internals (
App_Code/, App_Data/, bin/, obj/) are not served by IIS handlers. Source code, database scripts, and compiled binaries cannot be retrieved over HTTP.
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, SHA-256, PBKDF2) the application relies on for ticket protection, ViewState, and Remember-Me token hashing.
Database-Layer Defenses
Parameterized Query Discipline
- Every new database call in the application flows through a single centralized access layer (
DatabaseHelper) whose every public method requires a parameterized SQL string and a SqlParameter 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.
- The 131 ad-hoc SQL call sites that existed in older page code-behinds were migrated to
DatabaseHelper parameterized queries during the May 2026 security pass — high-risk import paths that previously concatenated raw form text into SQL are now fully parameterized.
- SQL strings in application code use named placeholders (
@UserID, @FID, 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 bulk-edit dropdowns) 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.
Reader and Row Accessors
- 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.
Per-Record Ownership at the Stored-Proc Layer
- Stored procedures that mutate Categories, Targets, and Methods require a
@UserID argument and include it in every UPDATE and DELETE filter. A bug in the application-layer access check would produce zero affected rows rather than another user’s data being modified.
Connection Pool Management
- The database connection pool is sized through the connection-string options. 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.
- Every database command carries an explicit 30-second timeout (configurable without a code change), so a slow or stuck query is abandoned rather than pinning a request thread indefinitely.
- The connection string is read once at class-load time from a single
<connectionStrings> entry. 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.
Defense-in-Depth Posture
Layered Authorization
- A single state-changing Checklist or Setup operation passes through up to four independent permission checks before any data is modified: (1) the AJAX endpoint validates the X-QFIT-CSRF token; (2) it re-derives the authenticated user from session or the Forms Authentication ticket; (3) it consults the page-level
BasePage.VerifyAuthenticatedOrShow() safety net; (4) the stored procedure itself includes WHERE UserID = @UserID. Any single layer being bypassed by a future code change still leaves three independent layers in place.
- Authorization queries are constructed so that an unauthorized record ID does not produce a database error or expose row counts — the query simply returns zero rows and the operation reports a generic “not found or access denied,” identical to the response the client receives for a non-existent ID.
Layered Input Defense
- A single user-supplied string passes through up to four independent defenses before being rendered: (1) ASP.NET request validation rejects obvious script-injection patterns at the framework boundary; (2) the application caps the length to the underlying database column size via
MaxLength on the textbox plus a server-side length check; (3) the database column refuses values that exceed its declared length; (4) the rendering site calls HttpUtility.HtmlEncode immediately before writing to the response.
- A Content-Security-Policy that blocks cross-origin script execution and inline event handlers acts as the final containment layer for any value that somehow survives all four upstream checks.
Layered Transport Defense
- HTTPS exclusivity is enforced by two independent mechanisms: (1) the Plesk SSL terminator handles HTTPS at the network edge; (2) the HSTS header instructs browsers to refuse plain-HTTP downgrades for one year, covering every subdomain. A commented
<rewrite> rule in Web.config is ready to enable a third HTTP-to-HTTPS redirect layer at the IIS rewrite stage when the production environment is configured to receive plain-HTTP traffic.
- Both the session cookie and the authentication cookie are
SameSite=Lax to prevent cross-site delivery even on a misconfigured intermediary.
Layered Error Containment
- An unhandled application exception is contained by three independent mechanisms: (1)
Global.asax.Application_Error intercepts the exception, logs it with a correlation ID, 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) a loop-breaker in Application_Error short-circuits exceptions raised during Error.aspx rendering itself, preventing an infinite 302 chain on Plesk.
Independent Failure Domains
- The logging, email-notification, and CSRF-violation-reporting 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 Checklist change; if the SMTP server is unreachable, a credential-stuffing alert or recovery email fails silently while the user’s action still completes.
Symmetric Failure Responses
- Failed login messages do not distinguish between “username does not exist” and “password incorrect.” The visible error message and the HTTP status code are intentionally identical — an attacker cannot enumerate which usernames exist on the platform.
- Record-access denied and record-not-found responses are identical from the user’s perspective. An attacker cannot determine whether a Category, Target, or Method ID exists at all by probing the endpoint with arbitrary numeric values.
- The 401 Unauthorized HTTP status is silently promoted to 403 Forbidden by the global error handler, so an attacker cannot distinguish “file does not exist” from “file requires authentication.”
Your Data & Privacy
Data You Provide
- qFIT stores the data you enter into your Checklist, Routines, Goals, and preferences. We do not sell or share this data with third parties.
- The shared
plp_Logs audit table records security and operations events (login, logout, exceptions, throttle hits, page visits) tagged with App = "qFIT"; it is used only to investigate incidents and is never exposed to other users.
- The email address you provide for password recovery is used only to send recovery emails and incidental account notifications. It is not shared with any external service beyond the SMTP relay used to deliver the message.
Coordinated Disclosure
- If you believe you’ve found a security issue, please report it via Contact Us and it will be looked at directly.