Skip to content

v5.51.1: Security hardening backports and an Action Filter evaluation fix

A security patch for the certified 5.x line. Every hardening change here was first reviewed and merged on the development line and is backported unchanged in behavior for legitimate inputs; regression suites in each affected package pin the new behavior. All 298 packages move to 5.51.1 in lockstep and the line stays ISC-licensed. Two of the changes do alter behavior for deployments that had opted into specific authentication settings — see Upgrade Notes before upgrading.

  • SQL filter validation tightened (@memberjunction/global, @memberjunction/core, @memberjunction/generic-database-provider): RunView’s ExcludeUserViewRunID — a GraphQL string input — was interpolated raw into the view WHERE clause with no validation, unlike every sibling clause (ExtraFilter, UserSearchString and OverrideExcludeFilter all pass through ValidateUserProvidedSQLClause). The value is only ever a UserViewRun GUID, so it is now rejected unless it is a well-formed GUID, closing an authenticated injection sink that bypassed entity permissions and row-level security. ValidateUserProvidedSQLClause additionally denies WAITFOR, the time-based blind-injection vector, and SQLExpressionValidator now denies references to database system catalogs and metadata objects (sys.*, INFORMATION_SCHEMA, syslogins, pg_catalog.*, pg_authid/pg_shadow/pg_user/pg_roles) in all validation contexts, full_query included — these objects sit outside MemberJunction’s entity-permission model, so permitting them turned a validated SELECT into a schema-enumeration primitive. String literals are stripped before the check runs, so a literal value such as 'sys.x' is still allowed. The intended subquery capability of ExtraFilter is unaffected.
  • SQL literal-stripper bypass closed (@memberjunction/global, @memberjunction/core): both of MJ’s SQL screens — ValidateUserProvidedSQLClause (guarding ExtraFilter, OrderBy and UserSearchString) and SQLExpressionValidator (guarding Aggregates and ad-hoc queries) — stripped string literals with a regex that honored backslash escaping. SQL Server and PostgreSQL do not treat \ as an escape, so a payload shaped like x = 'a\') ; DROP TABLE Users; --' was swallowed whole as one “literal” and stripped away before the keyword denylist ran, while the database closed the literal at the real quote and executed the stacked statement. Both screens now share a single StripSQLStringLiterals helper implementing SQL-standard doubled-quote ('') semantics.
  • Values parameterized and escaped in SQL text-building paths (@memberjunction/server, @memberjunction/core, @memberjunction/generic-database-provider): ReportResolver.CreateReportFromConversationDetailID now binds ConversationDetailID through a parameterized mssql request as a UniqueIdentifier rather than interpolating it into the query string. GenericDatabaseProvider.CheckRecordRLS escapes embedded single quotes in primary-key values before building its WHERE clause, mirroring the escaping already present in the Load() path. RowLevelSecurityFilterInfo.MarkupFilterText escapes embedded single quotes in substituted user-property values and treats undefined the same as null/object, leaving the token unresolved instead of substituting the literal string "undefined".
  • Authentication hardening (@memberjunction/server, @memberjunction/ai-mcp-server): the superadmin MJ_API_KEY comparison in getUserPayload was a plain ===, which short-circuits on the first differing byte and leaks a timing side channel; both sides are now hashed to fixed-length SHA-256 digests and compared with timingSafeEqual. JWT verification explicitly pins the accepted signature algorithms to the asymmetric family (RS256/RS384/RS512, ES256/ES384/ES512, PS256) on both MJServer’s issuer path and MCPServer’s JWKS path — defense in depth against alg=none and RS256-to-HS256 confusion.
  • OAuth callback handler hardened (@memberjunction/server): caller-supplied connectionId was interpolated into a raw ExtraFilter without escaping and is now validated as a UUID at the request boundary and escaped at the SQL sink. frontendReturnUrl was redirected to after only a URL-parse check, making the callback an open redirect from the trusted MJAPI origin; its origin is now validated against cors.allowedOrigins plus the built-in redirect origins, both when the flow is initiated and when the redirect is issued. Each rejection is logged with the offending URL.
  • API-key lookup invariant enforced at the sink (@memberjunction/api-keys): ValidateKeyByHash now asserts its argument is a SHA-256 hex digest before building the SQL filter, holding the injection-safety invariant for all present and future callers.
  • Action Filter evaluation implemented (@memberjunction/actions): ActionEngineServer.RunSingleFilter was a return true stub, which made every Action Filter a no-op. Filters now resolve via a registered BaseActionFilter subclass (through ClassFactory, keyed by filter ID) or by evaluating the filter’s Code column with an ActionFilterContext, cached per row version. Failure semantics are fail-closed: a filter that throws, yields a non-boolean, or has no evaluable logic prevents the action and logs the reason. No shipped metadata contains ActionFilter rows, so no existing behavior changes.
  • userHandling.newUserAuthorizedDomains now matches email domains, not request origins. The new-user domain gate previously authorized against the hostname parsed from the request’s Origin header, which is trivially spoofable on non-browser requests: a holder of any valid IdP token could auto-provision an account under an authorized domain by forging Origin. It now authorizes against the email domain of the verified identity token. The gate is off by default (newUserLimitedToAuthorizedDomains: false, newUserAuthorizedDomains: []), so deployments that never enabled it are unaffected. If you did enable it, review the list before upgrading: entries that are frontend hostnames (app.example.com, localhost) must be replaced with the email domains your users sign in with (example.com); wildcards match in full, so *.example.com matches mail.example.com but not example.com — list both if you need both; and identity providers that issue a bare username with no email claim can no longer auto-provision, with the denial logged explicitly. Relatedly, MCP OAuth auto-provisioning previously also required a non-empty request Origin to enter the check at all, and MCPServer’s resolveOAuthUser passes no request domain — so with the gate enabled, MCP OAuth users could never be auto-created. Now that the spoofable precondition is gone, an MCP OAuth user whose JWKS-verified token carries an authorized email domain plus given/family name claims will be auto-provisioned, consistent with the browser path. If you were relying on that side effect to keep MJ user records from being created, add the restriction explicitly.
  • The OAuth return-URL allowlist is derived from cors.allowedOrigins. Deployments on the default ['*'] are unaffected — every return URL is still allowed. Deployments that have narrowed cors.allowedOrigins are mostly self-protecting, since a browser frontend must already be on that list to call /oauth/initiate at all, but three cases can now fall back to MJAPI’s built-in page instead of returning to the app: a return URL on a different origin than the caller, a server-to-server initiate whose return origin was never CORS-listed, and any proxy setup where the browser-visible origin differs from the configured one (matching is exact on scheme, host and port).
  • Stored MJ Queries that introspect database catalogs will fail validation. The system-catalog denial applies in every validation context including full_query, with no opt-out by design. Replace such queries with MJ’s own entity metadata, which respects the permission model.
  • JWT verification accepts only asymmetric signature algorithms. A deployment whose identity provider signs tokens with HS256 (shared-secret HMAC) now fails closed at authentication, in MJServer and the MCP server alike. Standard OIDC providers (Auth0, Entra, Okta) sign RS256 and are unaffected. The list is hardcoded deliberately — it forecloses alg=none and RS256-to-HS256 confusion.
  • Negation-form RLS filters (<>, NOT IN, NOT LIKE) now match fewer rows when the substituted user property is undefined. Previously the literal string "undefined" leaked into the filter and widened results, so fewer visible rows after upgrading is the fix working rather than a regression.