Tips

  • 🛡️ Found a vulnerability? Report it via Contact Us (Security Issue) — these get top priority.
  • ☰ Use the floating On this page ☰ button (bottom-right) to jump straight to any card, or just tap a card below to expand it.
  • 🙈 You control your own privacy: hide your age, gender, or shorten your display name in Preferences.
  • 🔐 Cards cover topics like: site-wide, login, chat, events & groups, account & session, member matching, contact form, and PWA security.
Site-Wide Security

Data Protection

  • All database queries use parameterized statements, which prevents SQL injection attacks.
  • User-provided content is HTML-encoded before being displayed, preventing cross-site scripting (XSS) attacks.
  • Server-side validation is performed on all inputs independently of any client-side validation.
  • Field length limits are enforced server-side against actual database column sizes.

Authentication & Session Management

  • Authentication uses an encrypted and cryptographically signed Forms Authentication ticket.
  • If your session data is lost (for example, after a server restart), it is automatically rebuilt from your valid authentication ticket in Global.asax — you remain logged in without re-entering your credentials.
  • Sessions use non-default cookie names (QMEETSESSION and .QMeetAuth) 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 via PopulateSessionForAuthenticatedUser 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 via IsEventMemberAndNotBanned, IsEventOrganizerAndNotBanned, and their group counterparts.

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, motion sensors, Topics API). Even a fully compromised script running on the page cannot prompt for access to these capabilities.
  • 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 Spectre-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 proxied via X-Forwarded-Proto by Plesk), 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 '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.
  • 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 IIS 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.
  • EventValidation is enabled site-wide. The framework records the set of legitimate postback target IDs and event values when a page is rendered, and rejects any postback containing an event from a source not present in that original set. Attempts to fabricate postback events for hidden or disabled controls are rejected at the framework level before any application code runs.
  • ViewState is bound to your current session via ViewStateUserKey, which is set in the OnInit stage of every page that inherits BasePage — 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.

Input Filtering

  • String values processed by application code are passed through a centralized sanitization step (SecurityHelper.Sanitize) that trims leading and trailing whitespace and truncates the value to a field-specific maximum length before storage. This normalization is applied uniformly via a shared helper rather than ad hoc per page, ensuring consistent enforcement across all input paths.
  • All user-supplied strings rendered back to the browser are passed through HttpUtility.HtmlEncode() at the render site — not at the input boundary. Output encoding is applied where the data is consumed (the page rendering it), so a record that was inserted into the database before output encoding was applied at that location is still safely rendered now.
  • 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, the IP address shown on the login page, error messages echoed back from validation, and feedback text rendered for moderation.
  • validateRequest="false" is set site-wide because chat and feedback fields legitimately accept <, >, and quote characters. XSS defense 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 (cookieless="UseCookies"). This prevents tokens from appearing in server logs, browser history, address-bar copies, or HTTP Referer headers when following an outbound link.
  • The Forms Authentication ticket is protected with protection="All" — the cookie payload is both AES-encrypted (so its contents cannot be inspected) and HMAC-signed (so a single-byte modification invalidates the entire ticket). Tampering, partial forgery, and replay-after-decryption attempts all fail at the framework layer before any application code reads the ticket.
  • 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 (QMEETSESSION) and the authentication cookie (.QMeetAuth) are set with SameSite=Lax. Browsers will not send these cookies on cross-site POST requests or with cross-site embedded resources, blocking a wide class of CSRF attacks at the cookie-delivery layer.
  • A site-wide default in <httpCookies> applies HttpOnly=true and SameSite=Lax to every cookie the application emits, including cookies created without an explicit policy. A developer cannot accidentally introduce a JavaScript-readable cookie by forgetting to set the flag.
  • The Secure flag is applied to every outbound cookie on HTTPS requests by Global.asax Application_EndRequest. Because Plesk terminates SSL before IIS, the framework's Request.IsSecureConnection returns false on the backend; the EndRequest pass reads X-Forwarded-Proto via SecurityHelper.IsRequestSecure and explicitly sets Secure=true when the request is real HTTPS.
  • 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.

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 via SecurityHelper.IsThrottled. 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 via SecurityHelper.GetClientIP, enabling accurate client attribution when traffic passes through Plesk'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 (plp_Logs) 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 flat-file (App_Data/last_errors.txt, ~5 MB rotation) — this catches crashes that happen mid-response-flush where the redirect to Error.aspx cannot complete cleanly. The file lives under App_Data, which IIS 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 ?cid= query string is rendered only when it matches a strict ^[A-Z0-9]{1,16}$ allowlist pattern; 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 plp_Logs table via LoggingService. Distinct severity levels (INFO, WARN, ERROR, CRITICAL, AUDIT, SECURITY) and category tags (Login, LoginFailed, AccessDenied, SecurityViolation, Suspicious, Delete, Update, Exception, PageVisit, ClientData) make it possible to filter the audit trail to a specific event class.
  • When you visit key pages, your IP address, browser, device type, OS, screen resolution, language, and timezone are recorded. This information is used to 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.
  • Redirect-driven ThreadAbortExceptions 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 Web.config flag. When enabled, Application_PostAuthenticateRequest in Global.asax intercepts every request — pages, AJAX WebMethods, .ashx handlers — and redirects to Maintenance.aspx with HTTP 503 + a Retry-After header.
  • The maintenance page itself plus the static assets it depends on (Content, 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 invokes FormsAuthentication.SignOut() to revoke the authentication ticket, then Session.Clear(), Session.RemoveAll(), and Session.Abandon() to wipe and tear down the server-side session container.
  • Beyond the framework calls, the authentication cookie (.QMeetAuth) and the session cookie (QMEETSESSION) are each explicitly overwritten with an empty, past-dated value. The replacement auth cookie is marked HttpOnly, SameSite=Lax, and Secure whenever the originating request was genuine HTTPS (read from X-Forwarded-Proto via SecurityHelper.IsRequestSecure, since Plesk terminates TLS before the request reaches the application). This belt-and-suspenders deletion forces client-side removal even on browsers that do not reliably honour cookie removal from SignOut()/Abandon() 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 sign-out 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 enableSsl setting in Web.config mandates encryption — plaintext delivery is not possible.
  • SMTP server credentials are read from Web.config at runtime via the framework's system.net/mailSettings section. The SmtpClient instances used in application code do not contain hardcoded credentials.
  • The SMTP client timeout is explicitly capped at 15 seconds (overriding the framework default of 100 seconds) in EmailService. 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 wrapped in Using blocks so the underlying network socket is closed immediately after the send completes, even if an exception bubbles up from the SMTP transport.

Built-in Provider Isolation

  • The legacy ASP.NET built-in Membership and Role providers have been explicitly removed via <clear /> directives in Web.config. No application code or framework component can accidentally invoke default provider implementations.
  • The ASP.NET RoleManager is disabled entirely (enabled="false"). 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, WebMethods, 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 assignment (window.qmeetCsrfToken) injected by BasePage at render time; it is never written to a cookie, never embedded in a URL, and never reflected into the DOM as an attribute.
  • The shared AJAX helper (QMeet.callMethod) reads the token on every call and forwards it as a custom X-QMeet-CSRF request header. State-changing WebMethods and HTTP handlers (chat, ContactUsStatus, etc.) invoke SecurityHelper.ValidateCsrfToken() 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 HttpUtility.JavaScriptStringEncode, 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 Guid.NewGuid(), 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 (<machineKey> with validation="HMACSHA256" and decryption="AES") rather than being auto-generated per worker. A MAC-signed ViewState payload or an encrypted Forms 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 honoured by a sister application.
  • The modern Cryptography.EncryptString / DecryptString 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 (FeedbackDate, ClosedDatetime, etc.) are converted to the user's local time zone only at render time, using a TimeZoneInfo.Id from the user's profile. Conversion is performed inside a try/catch via DateHelper.ToUserLocalTime — an unrecognised 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. 12/26/2026 5:31 PM) via the shared DateHelper 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 via SecurityHelper.IsThrottled. 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 (ThrottleLoginStuffing_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 (ThrottleLogin_MaxAttempts, ThrottleLogin_WindowMinutes), 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: Session.Abandon() releases the server-side session container, 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 via LoggingService.LogLogin.
  • Your IP address, browser, device type, OS, screen resolution, language, and timezone are recorded when the login page loads, providing context for incident reconstruction.
  • The IP address displayed on the login page is HTML-encoded before rendering, preventing a specially crafted IP header from being used as a reflected XSS vector.

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, 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 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 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 sign-in clears both counters.
  • A user who has deactivated their own account cannot silently sign 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 (qmeet_remember) carries a long, random single-use 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 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.
  • 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 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.

Remote Sign-Out (Sign Out Everywhere)

  • Each session carries a server-side security stamp stored in an HttpOnly cookie. The “sign 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 UserID 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 Urgent, Pin, Save, and Notify (SMS) flags. The recipient independently controls Pin / Save / Hide / Snooze / DateRead 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 MessageID 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 ChatFileUpload.ashx, an inline-VB HTTP 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 signed 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 with FileShare.None (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 (ChatFilesReady.ashx) returns files only from the requesting user’s own upload area and passes every stored filename through both HttpUtility.HtmlEncode 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 SuppressFormsAuthenticationRedirect set (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 FileStream opened with FileShare.None, 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 ChatBarStatus.ashx. The handler 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. IsEventMemberAndNotBanned, IsEventOrganizerAndNotBanned, and their group counterparts 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 EventID and GroupID 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 EventID for a private event you don't belong to — are logged as security violations via LoggingService.LogSecurityViolation with your user ID, IP address, and the EventID/GroupID you attempted to reach.

Public vs. Private Events & Groups

  • Each event and group carries an IsPublic flag. Public entities are visible to anonymous visitors via the EventSearch / GroupSearch pages. Private entities require authenticated membership 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 GroupID 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.

Review Before Visibility

  • New events, new groups, new interest categories, and new sub-categories all default to a pending state (IsApproved = pending) and are not made visible to other members until they have been reviewed. An unapproved entity is hidden from every non-organizer, even via a direct URL.
  • An event's organizer can preview their own entity before it is approved (so they can verify it looks right), but no other user sees an unapproved entity.

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 ShowListOfMembers flag set by the organizer. When false, 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 StartDateTime, computed server-side. A user cannot post a reminder time outside their own RSVP record.

Organizer Permission Surfaces

  • Manage pages (EditEvent.aspx, EditEventMembers.aspx, EditGroup.aspx, EditGroupMembers.aspx) call IsEventOrganizerAndNotBanned / IsGroupOrganizerAndNotBanned in Page_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 UserID.

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 InProc sessions are lost if the server's application pool restarts. When this happens, Global.asax.Application_AcquireRequestState detects the missing session and automatically reconstructs it from your still-valid Forms Authentication ticket by re-querying your user profile, role, and preferences from the database. You remain logged in and your work is not interrupted.
  • The session rebuild process only succeeds when a valid, non-expired, cryptographically intact authentication ticket is present. A missing, expired, or tampered ticket results in an anonymous session, not a reconstructed one.
  • Session rebuilds are logged with the user ID and timestamp via LoggingService. 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 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 SecurityHelper.GetAuthenticatedUserID. The framework populates this principal regardless of session state.
  • The cold-session fallback restores Session("UserID") from the validated FormsAuth principal so subsequent calls in the same logical session no longer have to repeat the fallback.
  • The fallback only succeeds when the FormsAuth ticket is present, decryption succeeds, and the embedded identity parses as a positive integer matching a known user. A missing, expired, or tampered ticket results in an unauthenticated response, not a recovered session.

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 honoured 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 ForgotLogin page accepts an email address or phone number, looks up the matching account if any, and emails or texts the user with either a username reminder or a new temporary password. 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.
  • Recovery requests are rate-limited per IP via ThrottlePasswordReset_MaxAttempts and ThrottlePasswordReset_WindowMinutes. 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.
  • When the recovery path issues a password reset, the replacement password is generated server-side and delivered only to the address or number already on file — the anonymous recovery form never accepts a caller-chosen replacement, so it cannot be used to set a known password on someone else’s account.
Member Matching & Discovery Security

Access Control

  • The Member Match and Contacts 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.

Contacts & Blocking

  • The Contacts 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 UserID and the target's UserID 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 via SecurityHelper.IsThrottled: 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 (ThrottleContactUs_MaxAttempts) and time windows (ThrottleContactUs_WindowMinutes) 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-click on Submit while a slow network is being recovered).

File Attachment Allowlist & Size Limits

  • Attached files are restricted to 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 Path.GetFileName() to remove any directory components, and the stored name is prefixed with the database-generated FeedbackID.
  • The destination path is fixed to the application's FeedbackFiles/ 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 Validation.IsEmailAddress. Phone numbers are stripped of formatting punctuation and validated by Validation.IsPhoneNumber.
  • The comment body is capped server-side at 4000 characters by an explicit length check — a multiline textbox’s MaxLength 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 ContactUs 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 ContactUs.aspx page. Anonymous submissions get the same throttling and validation as authenticated submissions; the only difference is the absence of a UserID 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 .aspx pages, the chat and keep-alive .ashx 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 resolves e.respondWith() 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 (OneSignalSDKWorker.js, 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 localStorage. 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 QMeet.callMethod() 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 set Response.SuppressFormsAuthenticationRedirect 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 sets TrySkipIisCustomErrors, so neither a proxy nor the browser can serve a stale “still authenticated” answer and an IIS 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-click cannot double-fire a state-changing WebMethod — 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

  • Buttons marked with data-save-btn="true" or .button-save are intercepted by a centralized click handler that adds a “saving” CSS class and a spinner immediately on click. 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

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

Per-Request CSRF Header Injection

  • The shared AJAX helper reads the per-session CSRF token from a single global variable (window.qmeetCsrfToken) injected by BasePage 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 ajaxPrefilter 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

  • The only values written to localStorage or sessionStorage by qMEET are scroll-position integers, UI preference strings (dropdown selections, theme), and one-shot post-login modal-dismiss flags. No authentication token, no session identifier, no email address, no event data, and no chat content 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 IIS X-Powered-By header, the ASP.NET X-AspNet-Version header, the IIS Server header (via removeServerHeader="true"), and the framework version banner (enableVersionHeader="false") are all suppressed. Automated scanners cannot fingerprint the platform from response headers.
  • Detailed IIS error pages are restricted via httpErrors errorMode. Remote clients receive the generic application error page even when an unhandled IIS-level error occurs.

Layered Error Routing

  • HTTP status codes 404 and 500 are routed to the application's generic error page through two independent mechanisms: the ASP.NET <customErrors> block in system.web handles errors raised inside application code, while the IIS <httpErrors> block in system.webServer handles errors raised before application code runs.
  • Global.asax Application_Error 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 IIS 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 Authorisation

  • Asset folders (Content/, Scripts/, Images/, Sounds/) are explicitly opened to anonymous access through scoped <location> elements in Web.config. Public root-level files (favicon.ico, manifest.json, service-worker.js, OneSignalSDKWorker.js, robots.txt, sitemap.xml, offline.html) are similarly opened.
  • 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.

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-scoped .well-known/web.config that grants access to all users — necessary because ASP.NET refuses a root <location> element 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 System.Security.Cryptography namespace.
  • Compilation runs with Option Explicit On — every variable must be declared before use. Option Strict Off is preserved for legacy implicit conversions (e.g. Session("UserID") Object → Integer) but is on the roadmap to tighten.
Database-Layer Defenses

Parameterised Query Discipline

  • Database access flows through three shared DAL classes — pm.DatabaseFunctions_pm, DatabaseFunctions_events, and DatabaseFunctions_chat. Code-behinds never open SqlConnection directly.
  • For ad-hoc queries against user input, the parameterised helpers RunAdHocSelectQueryWithParams and RunAdHocQueryWithParams are the required path. They accept a Dictionary(Of String, Object) of @Name bindings; helpers handle DBNull coalescing.
  • Legacy 8-argument RunAdHocSelectQuery / RunAdHocQuery calls still exist with fully static SQL (no concatenated user input). New code uses the parameterised 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 VARCHAR column sizes defined in the database schema.
  • Numeric and date fields are parsed through Integer.TryParse / DateTime.TryParse before being passed as parameters. A non-numeric or non-parseable date value is rejected at the application layer with a user-friendly message rather than being shipped to the database as a malformed string.
  • Session-sourced values (e.g. Session("UserID")) are TryParse'd 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 ConnectionString entry in <connectionStrings>. There is no code path that constructs a connection string from concatenated input.
  • The connection pool is sized via the Max Pool Size=500 hint in the connection string, placing a hard upper bound on simultaneous database connections.
  • Database commands and connections are opened and closed inside Using blocks (in the DAL 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 (FeedbackDate, ClosedDatetime, LastLogin, log entry timestamps) take their value from GETDATE() / GETUTCDATE() on the SQL Server itself, not from the application server's clock and not from any client-supplied value. Clock drift between the web tier and the database tier cannot create a window in which audit timestamps disagree.

Shared Logging & Feedback Tables

  • Structured logging writes into the shared plp_Logs table (mirrors the table PacklistPRO uses). Each row carries an App column so qMEET rows are filterable from rows generated by other apps on the same database.
  • Contact Us submissions write into the shared plp_Feedback table with App='qMeet', so qMEET feedback is cleanly separated from rows generated by other applications on the same database.
Defense-in-Depth Posture

Layered Authorisation

  • 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 Forms Authentication ticket; (3) it calls IsEventOrganizerAndNotBanned / IsGroupOrganizerAndNotBanned against the entity table with both EntityID and UserID; (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.
  • Authorisation queries are constructed so that an unauthorised EntityID 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 via SecurityHelper.Sanitize; (2) the database VARCHAR column refuses values that exceed its declared length; (3) the rendering site calls HttpUtility.HtmlEncode 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 front-end Plesk SSL terminator redirects plain HTTP to HTTPS at the network edge; (2) the HSTS header instructs browsers to refuse plain-HTTP downgrades for one year, covering every subdomain; (3) the Forms Authentication ticket and session cookie are configured for SameSite=Lax and explicitly marked Secure via Application_EndRequest when the request is real HTTPS (trusting X-Forwarded-Proto via SecurityHelper.IsRequestSecure).

Layered Error Containment

  • An unhandled application exception is contained by four independent mechanisms: (1) Global.asax Application_Error intercepts the exception, logs it with full context and 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) the IIS <httpErrors> block catches errors that occur before application code runs and routes them identically; (4) every exception is also appended to App_Data/last_errors.txt 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 EventID exists by probing the endpoint with arbitrary numeric values.
Your Data & Privacy

Data You Provide

  • qMEET stores the events, groups, interests, contacts, chat messages, and preferences you enter. 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 = "qMeet"; 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.