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.