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 authentication ticket.
  • If your session data is lost (for example, after a server restart), it is automatically rebuilt from your valid authentication ticket by the application's global request handler — you remain logged in without re-entering your credentials.
  • Sessions use non-default cookie names (one for the session and one for the authentication 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.
  • Session data is loaded from the database once per request and held in memory for that request's lifetime, 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.
  • Event and group membership / organizer permissions are checked on every read, write, and delete operation by dedicated membership, organizer, and ban checks (one set for events and one for groups).

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 — disables browser APIs the application does not use (microphone, camera, payment, USB, motion sensors, Topics API), so even a fully compromised script running on the page cannot prompt for access to them. Geolocation is permitted only for qMEET's own origin (self), because qMEET uses it for optional check-ins and location-aware features — see the Privacy Policy for exactly how location is collected, who can see it, and how to turn it off.
  • Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Resource-Policy: same-origin — cross-origin isolation hints that strengthen the browser's process boundary against Specter-class attacks.
  • Strict-Transport-Security (HSTS): max-age=31536000; includeSubDomains — instructs browsers to use HTTPS exclusively for one full year, covering the entire qmeet.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 (including those forwarded by the hosting provider's SSL-terminating proxy, which identifies them with the X-Forwarded-Proto header), 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' https://*.onesignal.com blocks cross-origin AJAX/fetch calls except to the push-notification provider; frame-ancestors 'self' reinforces the X-Frame-Options block at the modern-CSP layer; form-action lets a form submit only to this site and its four sister sites (the login step that recognizes a device you already verified on one of them passes through there), so no form can be repointed to send user data to an outside 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.
  • Beyond the directives above, the Content-Security-Policy also constrains script-src, style-src, img-src, font-src, and frame-src. Scripts and frames are limited to the site's own origin plus the OneSignal push endpoints; every other third-party script, stylesheet, frame, or websocket origin is refused by the browser.
  • The deprecated X-XSS-Protection header is explicitly removed from every response. Its legacy filter could itself be abused to selectively disable legitimate script, and modern browsers ignore it; stripping it ensures no stale platform default re-introduces that risk. The Content-Security-Policy is the documented replacement.
  • 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 production web server's own configuration and the application's per-request handler (which covers 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. 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 by a per-user key that is set at the earliest valid point in the page lifecycle on every page. ViewState submitted in one user's session cannot be replayed by another user, even if the raw bytes are captured.

Input Filtering

  • String values processed by application code are passed through a centralized sanitization step that trims leading and trailing whitespace and truncates the value to a field-specific maximum length before storage. This normalization is applied uniformly via a shared helper rather than ad hoc per page, ensuring consistent enforcement across all input paths.
  • All user-supplied strings rendered back to the browser are HTML-encoded 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.
  • The output-encoding rule applies uniformly to every user-controllable value rendered into HTML: event/group names, descriptions, location and address fields, chat messages, interest labels, member display names, error messages echoed back from validation, and feedback text rendered for moderation.
  • The framework's request validation is relaxed on the specific pages where chat, feedback, and other content fields legitimately accept <, >, and quote characters. XSS defense on those pages relies on output encoding at the render site rather than on input validation at the request boundary.

Cookie & Token Handling

  • Authentication tokens and session IDs are never placed in URLs — they are stored in cookies only. This prevents tokens from appearing in server logs, browser history, address-bar copies, or HTTP Referer headers when following an outbound link.
  • The authentication ticket is protected with both encryption and signing — 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.
  • Cross-application authentication redirects are disabled, preventing authentication tickets from being accepted by other applications on the same server or domain.
  • Both the session cookie and the authentication cookie 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 applies HttpOnly=true and SameSite=Lax to every cookie the application emits, including cookies created without an explicit policy. A developer cannot accidentally introduce a JavaScript-readable cookie by forgetting to set the flag.
  • The Secure flag is applied to every outbound cookie on HTTPS requests at the end of each request. Because the hosting provider terminates SSL before the application, the framework's own secure-connection check returns false on the backend; the end-of-request pass reads the X-Forwarded-Proto header and explicitly sets Secure=true when the request is real HTTPS.
  • Expired session IDs are regenerated rather than reused, preventing session fixation attacks that rely on planting a known-expired identifier and waiting for it to be reissued.

Rate Limiting

  • Rate limiting is applied independently to multiple sensitive operations, each with its own attempt threshold and time window: login, password reset, contact form, account creation (Join), and invite sending.
  • 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 the hosting provider's reverse proxy. The direct connection address is used as a fallback when no forwarding header is present. This ensures throttle keys and security event logs attribute activity to the actual client rather than the intermediary.

Search Engine & Crawler Controls

  • Sensitive pages — including login, dashboard, chat, manage-event/group, account, and contact-us submission pages — carry noindex, nofollow directives. Search engines cannot index these pages or follow their links.
  • A site-wide robots.txt instructs all search engine crawlers to avoid account-, chat-, and management-related pages.

Error Handling

  • Friendly, generic error messages are shown to users — raw technical detail (stack traces, SQL text, file paths) is never exposed to ordinary users.
  • Every unhandled exception is logged to the audit trail with full request context — the request URL, HTTP method, user IP, session ID, and the complete exception chain including inner exceptions up to five levels deep — so failures can be investigated from the log without surfacing any detail to the user.
  • Every unhandled server error is assigned a correlation ID: the full identifier is written to the log row and a short 8-character token is shown on the error page, so a user can quote it to support and have the exact incident located — without the page ever exposing the underlying exception.
  • Every exception also appends to a size-capped, self-rotating diagnostic file on the server (~5 MB rotation) — this catches crashes that happen mid-response-flush where the redirect to the error page cannot complete cleanly. The file lives in a folder that the web server never serves over HTTP.
  • 404 Not Found errors are treated as a distinct case: logged at warning level with the requested URL and authenticated user context, then 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 short correlation token echoed onto the error page from the query string is rendered only when it matches a strict allowlist pattern (1 to 16 uppercase letters and digits); any value carrying markup or other characters is dropped, so the reflected reference token can never become a reflected-XSS vector.
  • Any transient diagnostic note stashed server-side for the error page is consumed and cleared on every error-page render — even when it is not displayed — so a detail captured during one request can never linger into a later, unrelated request, including on a shared device.

Security Logging

  • Security-relevant events are logged with IP address, browser information, and timestamp into the audit log. Distinct severity levels (INFO, WARN, ERROR, CRITICAL, AUDIT, SECURITY) and category tags (such as login, failed login, access denied, security violation, suspicious activity, delete, update, exception, page visit, and client data) 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 identify and respond to unauthorized access attempts and to reconstruct incidents from the audit trail.
  • 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. Oversized inputs cannot trigger a database error mid-log-write and cannot be used as a log-injection or buffer-style attack vector.
  • Log entries generated from unauthenticated flows (account recovery, the contact form) record only a masked rendering of any email or phone the user typed (j***@gmail.com, ***-4567), so the audit trail supports abuse correlation without persisting recoverable personal data.
  • Exceptions that a redirect raises as a normal part of the framework's flow are filtered out of the audit writer, so the log isn't spammed with framework-normal exceptions and the trail stays focused on genuine events.

Maintenance Mode

  • The site can be placed in maintenance mode via a single site setting. When enabled, the application's request pipeline intercepts every request — pages, AJAX methods, HTTP handlers — and redirects to the maintenance page with HTTP 503 + a Retry-After header.
  • The maintenance page itself plus the static assets it depends on (styles, scripts, images, sounds, favicon, manifest, service worker, and PWA icons) are bypassed so the page always renders correctly with its styling and icons even while the rest of the site is closed.

Logout & Session Termination

  • Logging out revokes the authentication ticket, then clears, removes, and abandons the server-side session container to wipe and tear it down.
  • Beyond the framework calls, the authentication cookie and the session cookie are each explicitly overwritten with an empty, past-dated value. The replacement authentication cookie is marked HttpOnly, SameSite=Lax, and Secure whenever the originating request was genuine HTTPS (read from the X-Forwarded-Proto header, since the hosting provider terminates TLS before the request reaches the application). This belt-and-suspenders deletion forces client-side removal even on browsers that do not reliably honor cookie removal from the framework's log-out and session-abandon calls alone — otherwise the persistent ticket would silently rebuild the session on the next request.
  • Any legacy credential cookies left behind by older versions of the site are proactively expired at logout as well, so a stale value cannot survive the logout on a returning browser.
  • The logout event is logged — including user ID and IP address — before the session is cleared, ensuring the audit trail is never lost.

Outbound Email Security

  • All outbound email is transmitted over an encrypted SMTP connection using TLS on port 587. The mail configuration mandates encryption — plaintext delivery is not possible.
  • SMTP server credentials are read from the server's configuration at runtime. The mail-sending code does not contain hardcoded credentials.
  • The SMTP client timeout is explicitly capped at 15 seconds (overriding the framework default of 100 seconds) in the email service. A slow or unreachable mail server cannot pin a request thread for an extended period.
  • When an outbound email fails, the SMTP error detail (authentication failures, RFC 5321 reply codes, mail-host names) is logged server-side but never returned to the user — the caller receives only a generic “delivery failed” marker, so transport internals cannot leak into a user-facing message.
  • Email subjects are sanitized by stripping CR/LF and clamping length, so a user-supplied value flowing into a subject 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.
  • Outbound email message and SMTP client objects are disposed deterministically, so the underlying network socket is closed immediately after the send completes, even if an exception bubbles up from the SMTP transport.

Built-in Provider Isolation

  • The legacy ASP.NET built-in Membership and Role providers have been explicitly removed from the site's configuration. No application code or framework component can accidentally invoke default provider implementations.
  • The ASP.NET role manager is disabled entirely. Authorization decisions are made exclusively by application code reading from session data — the framework's built-in role infrastructure plays no part.

CSRF Protection for AJAX, Page Methods, and Handlers

  • Every authenticated request issued by the page receives a per-session CSRF token, generated as a cryptographically random GUID and stored only in server-side session memory. The token is exposed to client JavaScript through a single inline script assignment injected on every page render; 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 reads the token on every call and forwards it as a custom X-QMeet-CSRF request header. State-changing page methods and HTTP handlers (chat, the contact-form log, etc.) validate the token as their first action — before any database query runs.
  • Cross-origin AJAX delivery of the token is impossible: the X-QMeet-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 real call is sent.
  • The submitted token is compared against the stored session token in constant time — the comparison inspects every character regardless of where the first mismatch occurs, denying a network-adjacent attacker a timing side-channel for recovering the token byte by byte.
  • 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-QMeet-CSRF-Status: expired and the client transparently fetches a fresh token from a dedicated refresh handler and retries the call once. That refresh handler is itself authentication-gated and rate-limited to 20 requests per minute per user, so the recovery path cannot be abused to enumerate or brute-force tokens.
  • When the token is emitted into the page it is passed through JavaScript string encoding, so neither the token nor the adjacent localized message strings can break out of their JavaScript string literal and inject script into the page.
  • The token-refresh handler's JSON response is itself marked no-cache / no-store so neither a proxy nor the browser can retain a token value; it returns the (re-)generated session token JavaScript-string-encoded; and a client that exceeds the 20-per-minute ceiling receives an HTTP 429 with that breach recorded in the audit trail.

Cryptographic Discipline

  • Every per-session secret — the AJAX CSRF token, per-form login and contact-form CSRF tokens — is generated from a cryptographically strong GUID, which on .NET draws from the OS cryptographic random source. No security-relevant identifier is derived from a counter, timestamp, or predictable seed.
  • 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 (validation="HMACSHA256" and decryption="AES") rather than being auto-generated per worker. A MAC-signed ViewState payload or an encrypted authentication ticket therefore 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 qMEET, so a ticket minted here is never honored by a sister application.
  • The modern data-encryption helpers use AES-256-CBC + PBKDF2 (100,000 iterations, SHA-256) + per-record salt + IV + HMAC tag, in an encrypt-then-MAC construction: decryption recomputes the HMAC-SHA-256 tag over the salt, IV, and ciphertext and rejects the input on a constant-time mismatch before any plaintext is produced — closing both padding-oracle and tag-timing side channels. Legacy Rijndael/TripleDES helpers exist for backwards-compatibility with already-encrypted data but are not used for new encryption.

Time Handling

  • UTC timestamps from the database (such as feedback dates and closed dates) are converted to the user's local time zone only at render time, using the time-zone ID from the user's profile. Conversion is performed inside an error handler — an unrecognized time-zone ID falls back to server-local time rather than throwing a rendering exception.
  • Displayed dates and times use a consistent format (no leading zeros, e.g. Sat 12/26 @ 5:31 PM) via the shared date helpers, so a user-controlled value cannot influence the surface format and slip past parsing somewhere.
Login Page Security

Rate Limiting

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

Credential Protection

  • Failed login attempts show the same error message whether the username or the password was wrong. This prevents attackers from discovering which usernames exist on the platform.
  • After a successful login, your old session is destroyed: the server-side session container is released, and the session cookie is invalidated. A brand-new session and session ID are issued.
  • The password field is never pre-populated from any stored source.
  • After login, you are only redirected to pages within the qMEET application. Return URLs are validated against an open-redirect prevention pattern.
  • Passwords are never trimmed before comparison. A password that begins or ends with a space is stored and verified exactly as entered.
  • The username and password fields each have a server-side maximum length cap. Inputs that exceed these limits are rejected before any database query is issued.

Form Security

  • A per-session CSRF token is embedded in the login form and validated on every submission. The token is regenerated after each use, so replaying a captured form submission is not possible.
  • The CSRF token is a cryptographically random identifier — it is not predictable, sequential, or guessable.
  • The failed-attempt counter used for rate limiting is stored in server-side memory — it cannot be reset by clearing browser cookies, switching browsers, or opening a new session.

Audit Logging

  • Every login attempt — successful or failed — is logged with the IP address, browser, and timestamp.
  • Your IP address, browser, device type, OS, screen resolution, language, and timezone are recorded when the login page loads, providing context for incident reconstruction.

Browser Integration

  • The password field is a masked password input, 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, allowing modern password managers to fill credentials correctly while signalling to the browser that no other field should be confused for a password.

Account Lockout

  • Failed logins are counted in two independent stores: a fast in-memory counter and a persistent failed-attempt count and lock-expiry time on the account record. 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 persistent counter lives in the database rather than in memory, the lockout survives an application-pool recycle — an attacker cannot reset it by waiting out a restart. A successful login clears both counters.
  • A user who has deactivated their own account cannot silently log back in: after the password is verified, the login re-checks the account's active flag and blocks reactivation-by-login, returning a clear “account inactive” message.

Password Requirements

  • A server-side password policy is enforced independently of any client-side check: a minimum length (8 characters, 10+ recommended), at least three of the four character classes (uppercase / lowercase / digit / symbol), at least five distinct characters, no leading or trailing space, no common weak substrings (such as “123”, “abc”, “password”), and the password may not contain or equal the username.

Remember-Me Token

  • The optional “remember me” cookie carries a long, random single-use token. The token is hashed with SHA-256 before storage in the database — 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 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 logs out.
  • Token rows carry an explicit expiry and are rejected past it, 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.
  • 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.

Re-Authentication for Credential Changes

  • Changing your username or password requires re-entering your current password, even though you are already logged 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.
  • Changing the login email address on My Profile goes through the same gate and the same throttle. Password resets and login codes are sent to that address, so without the check an open session could redirect them and then reset the password. The rest of the profile saves without a password; the prompt appears only when the email address itself is being changed, and a refused change saves nothing.

Self-Service Account Deactivation

  • Deactivating your account is a reversible soft delete: it sets the account's active flag off on your own record only (after the current-password re-authentication and an explicit confirmation check) and preserves your data so it can be reactivated — nothing is hard-deleted. The database update is parameterized and scoped to your own user ID.
  • Deactivation also deletes every “remember me” token for the account and logs 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.

Remote Logout (Log Out Everywhere)

  • Each session carries a server-side security stamp stored in an HttpOnly cookie. The “log out everywhere” control rotates the stored stamp, which makes every other device's cookie stop matching on its next request — remotely terminating all other logged-in sessions even though the authentication ticket itself is long-lived.
Chat Security

Access Control

  • Every chat operation — loading messages, posting, marking read, deleting, pinning, hiding, snoozing — independently verifies that the requesting user is either the sender or the recipient of the affected row. Access is not assumed from the page having loaded successfully.
  • Sender vs. recipient ownership is enforced inside the SQL WHERE clause, not just at the application layer. An UPDATE or DELETE issued with the wrong user ID affects zero rows rather than another user's message.
  • Chat-management actions that go beyond your own message — adding or removing a participant, rerandomizing thread colors, deleting a thread — require that you are the thread's creator, and adding a participant additionally requires that the target is already one of your contacts. A non-creator request affects zero rows.

Per-Message Flags

  • The sender controls whether a message is sent with Time-Sensitive, Priority, Pin, Save, and Notify (push) flags. The recipient independently controls Pin / Save / Hide / Snooze / date-read for their own copy of the message. The sender's view and the recipient's view of the same message are updated through separate sets of columns, so an action by one user cannot rewrite the other user's view.
  • Each per-message action runs through a parameterized SQL statement keyed to the message ID and the requesting user's role on that message (sender vs. recipient). Tampering the request to claim the other role results in a zero-row update.

Chat File Uploads

  • File uploads in chat flow through a dedicated HTTP upload handler. The handler verifies the requesting user is authenticated, enforces size limits, and writes files into an upload area namespaced by the authenticated user's own ID — a user can never write into, or read out of, another user's upload folder — under randomized names rather than the submitted filename.
  • Original filenames are sanitized before being stored in the database — control characters and path separators are stripped, and the stored filename is prefixed with the message identifier so two uploads cannot collide on disk even with identical original names.
  • Per-file and total upload size are capped, and the limits are enforced server-side before any file is read.
  • The upload handler validates the CSRF token before it reads a single byte of the upload (returning 401 if not logged in, 403 on a bad token), so a forged cross-site upload never reaches the file-handling code.
  • Uploads are checked against an executable/markup extension denylist (because chat files are served back as static content, a stored script would be an XSS/RCE risk). A blocked-extension upload is rejected and logged as a security event recording only the extension and the basename length — never the attacker-chosen filename.
  • When a client does not pre-declare the upload size (a chunked transfer), the upload is streamed in small chunks into a file opened for exclusive access (no other process can read the half-written file) and is aborted — with the partial file deleted immediately — the instant it crosses the per-file ceiling, so the size limit cannot be evaded by withholding the content length.
  • Each stored file is given a random 32-hexadecimal-character suffix, and a new upload is compared against the message's already-pending files (matching base names with the random suffix removed) so the same file cannot be ingested twice into a single message.
  • When stored files are listed back for download, the serving handler returns files only from the requesting user's own upload area and passes every stored filename through both HTML encoding and URL-path encoding before composing the markup — so a filename that slipped a markup character past the upload sanitizer still cannot inject into the rendered file list.
  • That same listing handler is unauthenticated-safe and cache-safe: an unauthenticated request receives an HTTP 401 with the framework's login-page redirect suppressed (a clean status code, not a login-page redirect that would corrupt the JSON contract), the JSON response is marked no-cache, and every thumbnail URL carries a per-request cache-busting token (?v=…) — so a stale browser cache can never surface another user's just-replaced pending image.
  • Every uploaded file — not only the chunked-transfer path — is written through a file stream opened for exclusive access, so no other process can read a half-written upload while it is still being received.

Active-Contacts Indicator

  • The top-of-page indicator that shows recently active contacts and new chat messages is refreshed via a polling AJAX call to a dedicated status endpoint. The endpoint verifies the requesting user is authenticated and returns only the requester's non-blocked contacts who were active within the last hour — a blocked member never surfaces in another user's indicator.
  • The polling endpoint short-circuits with an HTTP 304 (Not Modified) when its payload is unchanged: it computes a weak ETag from the first 64 bits of a SHA-256 hash of the response body and returns 304 with no content when the client's If-None-Match matches, under a Cache-Control: private, must-revalidate, max-age=0 policy. It writes no audit rows of its own, so repeated fifteen-second polling can neither flood the audit table nor re-transmit an unchanged payload.
  • The same poll handler implements read-only session state rather than full read-write session state, so it never acquires the per-session write lock — a background poll cannot serialize behind, or stall, the user's own concurrent page actions.
Events & Groups Security

Access Control

  • Every event and group operation — viewing details, RSVPing, joining/leaving, editing, deleting — passes through an application-layer membership/organizer check. Dedicated member-and-not-banned and organizer-and-not-banned checks (for events and for groups) are called before any sensitive operation proceeds.
  • UPDATE and DELETE statements include the current user's identity in the WHERE clause so a bug in the application-layer check still results in zero rows affected rather than another user's data being modified.
  • The event and group IDs held in your session are re-verified against actual database membership on every sensitive operation. They are not assumed to be valid because they were set when you landed on a details page.
  • Unauthorized access attempts — such as passing an event ID for a private event you don't belong to — are logged as security violations with your user ID, IP address, and the event or group ID you attempted to reach.

Public vs. Private Events & Groups

  • Each event and group carries a visibility tier — Any Member, Only My Contacts, or Private. Any-Member entities are visible to anonymous visitors via the Find Events / Find Groups pages. The other two tiers require authenticated membership (or a mutual contact of the organizer) for any access — including reading the details page.
  • Anonymous attempts to view a private event redirect to login with a clear message rather than silently 404'ing — but the same response shape is also used when a private event simply doesn't exist, so an attacker cannot enumerate the IDs of private events through probing.
  • A private group's details page applies the same gate explicitly and independently of the ban check: it is shown only to a non-banned member (or the group's creator) and refuses access otherwise, specifically to stop group-ID brute-forcing. Banned users are bounced at multiple checkpoints — both at page entry and again inside the membership/RSVP load — so a ban cannot be slipped past by deep-linking to a sub-action.

Best-Effort Moderation

  • New and edited events, groups, and interests go live as soon as the organizer publishes them — there is no approval gate that blocks them. Instead, each one surfaces in an admin Moderation Queue for best-effort review, and an admin can remove anything inappropriate. A removed entity is hidden from every non-organizer, even via a direct URL.
  • New interest categories and sub-categories still default to a pending state and are not offered site-wide until an admin reviews them.
  • Drafts (not yet published) and content an admin has removed are hidden from everyone but the organizer, who can still preview their own entity to verify it looks right.

Ban Lists

  • Each event and group maintains a per-entity ban list. Banned users are blocked from viewing the entity's details, member list, and chat threads regardless of any prior membership.
  • A site-wide block list further prevents specific accounts from creating new events or groups or sending chat messages; the check runs early in each affected operation.

Member List Visibility

  • Each event and group has a “show list of members” setting chosen by the organizer. When it is off, the member list is hidden from all non-organizer users — including the count, the names, and the RSVP status of attendees.
  • Per-account display flags further govern how an individual's name, age, and gender appear in any list context — for example reducing a full name to first-name-plus-last-initial, or hiding age and gender — applied on top of the entity-level member-list visibility flag.

RSVP & Reminder Validation

  • RSVP form fields (Going / Maybe / Not Going and guest count) are constrained by server-side EventValidation, which rejects any value not present in the originally rendered control, and the guest count is additionally range-checked before the database write.
  • Per-attendee reminder times (up to three reminders per event) are stored as offsets from the event's start time, computed server-side. A user cannot post a reminder time outside their own RSVP record.

Organizer Permission Surfaces

  • The manage pages (edit event, edit event members, edit group, and edit group members) call the organizer-and-not-banned check for the event or group as soon as they load. A user reaching the URL without organizer status is redirected to the entity's read-only details page.
  • Bulk-add member endpoints accept email or phone-number lists and parse each line individually. Lines that don't validate as an email or phone are skipped without error, so a single bad line can't fail the whole import. Each successful add is logged with the actor's user ID.

Cancellation

  • Events can be marked Canceled by the organizer. A canceled event continues to render (so existing attendees can see the cancellation), but RSVP changes and chat are disabled.
Account & Session Security

Session Rebuild After Interruption

  • ASP.NET in-process sessions are lost if the server's application pool restarts. When this happens, the application's global request handler detects the missing session and automatically reconstructs it from your still-valid authentication ticket by re-querying your user profile, role, and preferences from the database. You remain logged in and your work is not interrupted.
  • The session rebuild process only succeeds when a valid, non-expired, cryptographically intact authentication ticket is present. A missing, expired, or tampered ticket results in an anonymous session, not a reconstructed one.
  • Session rebuilds are logged with the user ID and timestamp. Unusual patterns — such as repeated rebuilds from a single account within minutes — are visible in the audit trail for forensic review.

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 an AJAX page method or 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 authentication ticket by a shared helper. The framework populates this principal regardless of session state.
  • The cold-session fallback restores the session's user identity from the validated authentication principal so subsequent calls in the same logical session no longer have to repeat the fallback.
  • The fallback only succeeds when the authentication ticket is present, decryption succeeds, and the embedded identity parses as a positive integer matching a known user. A missing, expired, or tampered ticket results in an unauthenticated response, not a recovered session.

Per-User Display Privacy

  • Per-account display flags control how your identity is shown to other members — whether your name is reduced to first-name-plus-last-initial, and whether your age and gender are shown at all. These flags are honored at the rendering layer wherever your profile appears to others (Contacts, event and group member lists, and member-match results), so the display is consistent across every context.
  • Member lists never expose raw account identifiers; the name shown is composed from these display flags rather than echoed straight from the profile record, so even a member who appears in a list reveals only the fields they have chosen to show.

Account Recovery

  • The Forgot Login page accepts an email address or phone number, looks up the matching account if any, and emails either a username reminder or a single-use password-reset link. Delivery is to the address on file only — a phone number is used to find the account, never to receive anything. The on-page response is identical whether or not an account was found, so the form cannot be used to enumerate registered email addresses or phone numbers.
  • The reset link carries a 256-bit token from a cryptographic random number generator, of which only the SHA-256 hash is stored, so a database reader cannot reconstruct a working link. It expires in 45 minutes, and requesting a new one immediately expires any outstanding link for that account.
  • Recovery requests are rate-limited per IP address, with a configurable attempt limit and time window. The rate-limit counter is recorded before the lookup runs, so probing unknown addresses still consumes the attacker's budget rather than being free.
  • If the submitted email or phone matches more than one account, the request is treated exactly like a no-match — the same generic confirmation is returned — closing an enumeration channel that distinguishing the cases would otherwise open.
  • Because the recovery form is used by anonymous visitors, the email or phone they type is written to the audit log only in masked form, so a recovery attempt never persists a recoverable third-party contact detail.
  • Asking for a reset changes nothing by itself: the stored password is replaced only when the emailed link is opened and a new password is chosen, and only if it passes the same complexity rules as when joining. A request made by someone who is not you therefore has no effect on your account — the email can simply be ignored. Choosing the new password also clears any failed-attempt lockout and revokes every “remember me” token.
Member Matching & Discovery Security

Access Control

  • The Member Match and Contact List pages require authentication. Unauthenticated users are redirected to login before any member data is loaded.
  • For every result, the fields shown are composed from that member's display-privacy flags (see Per-User Display Privacy above) rather than echoed raw from the profile record, so a list result reveals only the name form, age, and gender that member has chosen to show.

Member Match (Interest-Based Ranking)

  • Member Match ranks other members by how many of your tagged interests they share. The query computes the overlap server-side; client-side code does not see the other user's full interest tag list except for the tags that overlap with yours.
  • The interest-overlap query is fully parameterized: each member's overlap count is computed with bound integer parameters, never by concatenating IDs into SQL, so the ranking surface offers no injection foothold. The fields shown for each ranked member are still composed from that member's display-privacy flags.

Contact List & Blocking

  • The Contact List page lets you favorite, block, or unblock other members. A blocked member cannot send you chat messages or appear in your contact results.
  • Each favorite/block/unblock action runs through a parameterized statement scoped to both your own user ID and the target's user ID in the WHERE clause, so the action only ever affects your own contact relationship and cannot be redirected to modify someone else's.
  • Member search caps its result set, excludes your own account, and binds the email/phone/name query terms as parameters rather than concatenating them into SQL.

Interest Tag Validation

  • A new interest tag submitted by a user is visible only to its creator until it has been reviewed, so an unreviewed label cannot leak onto other members' profiles or search results.
  • Tag IDs submitted as filter selections are individually parsed and validated as integers before being assembled into SQL IN clauses. Non-integer input is discarded before any value reaches the database.
Contact Form Security

Rate Limiting

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

Bot Detection

  • The form carries a hidden honeypot field invisible to humans — it sits inside a visually-hidden, aria-hidden="true" container, and the input itself carries tabindex="-1" (keyboard users skip past it) and autocomplete="off" (password managers will not auto-fill it and spring the trap on a real user). A submission that fills it is almost certainly automated: the event is logged as a suspicious security event, and the bot is shown the same confirmation page a real user sees while nothing is written to the database — so the trap is indistinguishable from success and gives the bot no signal to refine its evasion.
  • An identical comment resubmitted within the same session is rejected with a clear message, preventing accidental double-posts (such as a double-tap on Submit while a slow network is being recovered).

File Attachment Allowlist & Size Limits

  • Attached files are restricted by an explicit server-side denylist of executable/script extensions (.aspx, .ashx, .exe, .dll, .bat, .cmd, .ps1, .vbs, .js, .html, .htm, .svg, .xml, and others). The accept="image/*" attribute on the picker also restricts the file selection to images on the client side.
  • Per-file size is capped at 10 MB and total combined size at 50 MB. All checks run server-side before any file is read or stored.

Filename Sanitization & Path Hardening

  • Uploaded filenames are sanitized server-side: pound signs are stripped, the base filename is run through a path-stripping function to remove any directory components, and the stored name is prefixed with the database-generated feedback ID.
  • The destination path is fixed to the application's feedback-attachment directory. Path-traversal attempts in the submitted filename do not affect the final write location.

Input Validation

  • The feedback category (bug report, feature request, security issue, general feedback, etc.) is validated against a server-side whitelist of known-valid values before the submission proceeds. A manipulated or unexpected category value is rejected before it reaches the database or the outbound email system.
  • Email addresses, when provided, are validated by the shared email-address check. Phone numbers are stripped of formatting punctuation and validated by the shared phone-number check.
  • The comment body is capped server-side at 4000 characters by an explicit length check — a multiline textbox's maximum-length setting is not enforced by the browser, which is exactly why the server-side check exists — before the row is written to the database.
  • 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.

IP Block List

  • A small set of known-abuser IP addresses is hardcoded into the contact-form handler. Submissions from these IPs are silently dropped without reaching the database or triggering outbound email/file-save side effects.

Anonymous Submissions

  • Anonymous users may submit feedback via the public Contact Us page. Anonymous submissions get the same throttling and validation as authenticated submissions; the only difference is the absence of a user ID on the record.
Progressive Web App Security

Cross-Origin Request Isolation

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

HTTP Method Restriction

  • The service worker only intercepts GET requests. All other HTTP verbs (POST, PUT, DELETE, PATCH) bypass the service worker entirely and pass straight to the network. A state-changing request can never be served from cache or replayed from cache after an offline period.

Bypass List

  • The service worker maintains a case-insensitive bypass list for paths that must never be served from cache: /api/ (reserved namespace), dynamic pages, the chat and keep-alive endpoints by name, and /robots.txt and /sitemap.xml (so crawlers always receive a fresh copy). Requests matching these patterns go straight to the network.

Static Asset Caching

  • Static resources (CSS, JavaScript, PWA icons) use a cache-first strategy with a network fallback. Because these files are served with version identifiers, a cached copy accurately reflects a known-good deployment state.
  • The cache-write paths store only responses carrying a 200 OK status, so an error, redirect, or partial response is never written into the cache and re-served in place of the real asset.

Cache Version Management

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

Synthetic Offline Response

  • When the network is unreachable and no cached response is available, the fetch handler answers the request with a synthetic Response object rather than undefined. Browsers reliably surface this as an offline state instead of an opaque ERR_INVALID_RESPONSE.
  • Service worker registration is wrapped in a feature-detection check. Browsers that do not support service workers continue functioning normally over the network.

Push Notifications

  • Push notifications use OneSignal. The OneSignal SDK runs in its own service worker (a one-line import of the OneSignal CDN script). The OneSignal hosts are the only third-party origins the site's Content-Security-Policy permits at all: cdn.onesignal.com and api.onesignal.com for scripts, and *.onesignal.com (including the wss:// websocket) for network connections and frames. No other external origin is reachable.
  • Subscription is opt-in: a one-time post-login modal asks the user whether to enable push, and the choice is remembered in the browser's local storage. The user can disable push at any time from the Notifications page without affecting their qMEET session.
Client-Side & AJAX Defenses

Session Expiry Detection

  • Every AJAX call made through the shared AJAX helper inspects the HTTP status. A 401 Unauthorized, 403 Forbidden, or 440 Login Time-out response triggers a modal “session expired” prompt rather than failing silently.
  • The prompt offers two paths: re-authenticate (the return URL is preserved as the current page so the user lands back where they left off) or remain on the page in view-only mode. View-only mode disables save and delete buttons, preventing the user from accidentally attempting to submit a change that would silently fail under an expired session.
  • A keep-alive ping fires every four minutes on authenticated pages to keep the session warm. If the ping detects an expired session (401/403/440), the session-expired modal is shown immediately.
  • The AJAX, file-upload, and keep-alive endpoints suppress the framework's login-page redirect before emitting a 401/440, so an expired-session request receives a true status code and JSON body instead of a silent 302 redirect to the login page — which would otherwise corrupt the JSON contract the client expects and leak the login URL into the background request.
  • The keep-alive endpoint forcibly disables all caching of its authentication-state response (no-store, no-cache, Pragma: no-cache, Expires: 0, max-age=0, revalidate-all-caches) and tells the web server not to substitute its own error page, so neither a proxy nor the browser can serve a stale “still authenticated” answer and a server error page can never replace the JSON body.

Offline-Aware Saves

  • Offline detection does not trust the notoriously flaky navigator.onLine signal alone. An offline event is debounced and then confirmed with a lightweight network ping; user actions are only blocked when both the browser flag and the ping agree the network is truly gone — preventing spurious “you're offline” failures during cell-tower handoffs while still short-circuiting a genuinely doomed request before it is sent.
  • A site-wide offline banner appears at the top of the page when the browser fires the offline event. There is no scenario in which the user appears connected but their input is being lost.

Request Coalescing & Timeout

  • Identical concurrent calls (same endpoint and body) made through the shared helper share a single in-flight network request, so a double-tap cannot double-fire a state-changing method — an anti-double-submit guard at the transport layer that complements the save-button lifecycle.
  • Every AJAX call carries a 30-second client-side timeout, so a hung server surfaces a clear error instead of an indefinitely pending request that consumes a UI slot.

Save-Button Lifecycle

  • Save buttons are intercepted by a centralized handler that adds a “saving” CSS class and a spinner immediately on press. This prevents accidental double-submission during network latency.
  • The save lifecycle exposes explicit terminal states — saved, error, and offline — each represented by a distinct CSS class. A reader of the page state always sees the canonical outcome rather than a stale “Saving…” spinner left behind by an interrupted handler.

Destructive-Action Confirmation

  • Destructive actions raise a confirmation dialog before their default action proceeds. The dialog message comes from the element itself, so each destructive action carries context-specific wording rather than a generic prompt.

Per-Request CSRF Header Injection

  • The shared AJAX helper reads the per-session CSRF token from a single page-level variable injected on every render. On every call it sets the X-QMeet-CSRF request header. Every state-changing endpoint validates this header before doing any work.
  • A jQuery request prefilter attaches the same header to legacy jQuery AJAX calls, but deliberately skips GET, HEAD, and OPTIONS requests — the token is never appended to a safe/idempotent request or to a CORS preflight. Native fetch() callers set the header explicitly.
  • When a stale token triggers the transparent refresh-and-replay, the original call is retried exactly once; the second attempt is flagged non-retryable, so a genuinely forged or persistently rejected request can never spin the page into an infinite refresh loop.

No Sensitive Data in Local Storage

  • Values written to the browser's local or session storage by qMEET are limited to scroll-position integers, UI preference strings (dropdown selections), one-shot post-login modal-dismiss flags, and unsent-work recovery (the join-form and Contact Us drafts, your last live-chat message). No authentication token, no session identifier, and no password is ever cached client-side.
Transport & Platform Hardening

Transport Layer Encryption

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

Server Fingerprint Reduction

  • The X-Powered-By header, the X-AspNet-Version header, the Server header, and the framework version banner are all suppressed. Automated scanners cannot fingerprint the platform from response headers.
  • Detailed web-server error pages are restricted by configuration. Remote clients receive the generic application error page even when an unhandled server-level error occurs.

Layered Error Routing

  • HTTP status codes 404 and 500 are routed to the application's generic error page through two independent mechanisms: the framework's custom-error handling covers errors raised inside application code, while the web server's own error handling covers errors raised before application code runs.
  • The application's global error handler intercepts every unhandled application exception, logs the full context to the audit trail with a correlation ID, and routes the user to the same generic error page that the web server layer would use.
  • A postback whose ViewState fails validation — typically a page submitted across a deployment or after the session was renewed — is recovered gracefully rather than surfaced as a server error: the handler drops the stale submission, redirects back to the same page as a fresh GET (routed through the open-redirect guard), and shows a non-alarming “form refreshed” notice. Tampered or replayed ViewState lands in the same safe path instead of a raw error page.

Static Folder Authorization

  • Asset folders (styles, scripts, images, and sounds) are explicitly opened to anonymous access through narrowly scoped configuration rules. Public root-level files (the favicon, the app manifest, the service worker, the OneSignal worker, robots.txt, sitemap.xml, and the offline page) are similarly opened.
  • Framework internals (application code, application data, compiled binaries, and build output) are not served by the web server's handlers. Source code, database scripts, and compiled binaries cannot be retrieved over HTTP.

Coordinated Disclosure

  • qMEET publishes a machine-readable security-disclosure policy at /.well-known/security.txt (RFC 9116) with a dedicated reporting contact and an expiry date, so security researchers have an unambiguous, standards-based channel for reporting a vulnerability.
  • That file is reachable anonymously through a dedicated folder-level access rule that grants access to all users — necessary because the framework refuses a site-level rule for a dot-prefixed path — and the published file carries a future-dated Expires, a Canonical URL, and a Preferred-Languages directive for full RFC 9116 completeness.

Compilation Posture

  • The compiler targets .NET Framework 4.8. Cryptographic primitives (HMAC, AES-256, SHA-256, PBKDF2) used for ViewState, ticket protection, and the modern data-encryption helpers are sourced from the framework's built-in cryptography library.
  • Compilation requires every variable to be declared before use.
Database-Layer Defenses

Parameterized Query Discipline

  • Database access flows through three shared data-access classes (the general-purpose one, the events-and-groups one, and the chat one). Page code never opens a database connection directly.
  • For ad-hoc queries against user input, the parameterized helpers are the required path. They accept a dictionary of named bindings; the helpers handle null coalescing.
  • Older helper calls that take only a SQL string still exist, but only with fully static SQL (no concatenated user input). New code uses the parameterized variants.
  • Stored procedures are used for non-trivial writes and multi-table reads.

Schema-Driven Validation

  • Field length limits enforced server-side — for event/group names, descriptions, location and address fields, chat message bodies, feedback comment, email address, phone number, and similar — match the actual column sizes defined in the database schema.
  • Numeric and date fields are safely parsed (with a failure check) before being passed as parameters. A non-numeric or non-parseable date value is rejected at the application layer with a user-friendly message rather than being shipped to the database as a malformed string.
  • Session-sourced values (such as your user ID) are safely parsed to integers before being cast or used in queries — an empty or non-numeric session value cannot crash a query mid-render.

Connection Management

  • The connection string is read once from the site's configuration. There is no code path that constructs a connection string from concatenated input.
  • The connection pool is capped by a setting in the connection string, placing a hard upper bound on simultaneous database connections.
  • Database commands and connections are opened and closed in disposal blocks (in the data-access helpers) so connections are returned to the pool immediately after use — even if an exception occurs mid-query.

Server-Side Time Authority

  • Audit and timestamp values written to the database (feedback dates, closed dates, last-login times, log entry timestamps) take their value from the SQL Server's own clock functions, not from the application server's clock and not from any client-supplied value. Clock drift between the web tier and the database tier cannot create a window in which audit timestamps disagree.

Shared Logging & Feedback Tables

  • Structured logging writes into a shared audit-log table. Each row carries an application column so qMEET rows are filterable from rows generated by other apps on the same database.
  • Contact Us submissions write into a shared feedback table, tagged with the application name, so qMEET feedback is cleanly separated from rows generated by other applications on the same database.
Defense-in-Depth Posture

Layered Authorization

  • A single state-changing event/group operation passes through multiple independent permission checks before any data is modified: (1) the AJAX endpoint validates the CSRF token; (2) it re-derives the authenticated user from session or the authentication ticket; (3) it runs the organizer-and-not-banned check for the event or group against the entity table with both the entity ID and the user ID; (4) the UPDATE statement itself includes the user identity in its WHERE clause. Any single layer being bypassed by a future code change still leaves the others in place.
  • Authorization queries are constructed so that an unauthorized entity ID does not produce a database error or expose row counts — the query simply returns zero rows and the operation reports “not found or access denied,” identical to the response for a non-existent ID.

Layered Input Defense

  • A single user-supplied string passes through multiple independent defenses before being rendered: (1) the application trims and length-truncates it with the shared sanitization step; (2) the database column refuses values that exceed its declared length; (3) the rendering site HTML-encodes it immediately before writing to the response; (4) the response carries a Content-Security-Policy that blocks inline event-handler execution from any value that survived encoding.

Layered Transport Defense

  • HTTPS exclusivity is enforced by three independent mechanisms: (1) the hosting provider's front-end SSL terminator redirects plain HTTP to HTTPS at the network edge; (2) the HSTS header instructs browsers to refuse plain-HTTP downgrades for one year, covering every subdomain; (3) the authentication ticket and session cookie are configured for SameSite=Lax and explicitly marked Secure at the end of each request when the request is real HTTPS (trusting the X-Forwarded-Proto header).

Layered Error Containment

  • An unhandled application exception is contained by four independent mechanisms: (1) the application's global error handler intercepts the exception, logs it with full context and a correlation ID, and redirects to the generic error page; (2) the framework's custom-error handling routes any error that escapes the application handler to the same generic error page; (3) the web server's own error handling catches errors that occur before application code runs and routes them identically; (4) every exception is also appended to a diagnostic file on disk as a final-resort diagnostic.

Independent Failure Domains

  • The logging, email-notification, and audit-write paths are each wrapped in their own exception handlers. A failure in any one of these subsystems is silent to the user and does not prevent the user's original action from completing — if the audit log database is briefly unavailable, the user still posts a chat message; if the SMTP server is unreachable, a credential-stuffing alert or contact-form notification 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, the HTTP status code, and the response timing are intentionally similar — an attacker cannot enumerate which usernames exist on the platform.
  • Event/group access-denied and not-found responses are identical from the user's perspective. An attacker cannot determine whether a private event ID exists by probing the endpoint with arbitrary numeric values.