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.
Security
Section titled “Security”- SQL filter validation tightened (
@memberjunction/global,@memberjunction/core,@memberjunction/generic-database-provider):RunView’sExcludeUserViewRunID— a GraphQL string input — was interpolated raw into the viewWHEREclause with no validation, unlike every sibling clause (ExtraFilter,UserSearchStringandOverrideExcludeFilterall pass throughValidateUserProvidedSQLClause). The value is only ever aUserViewRunGUID, 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.ValidateUserProvidedSQLClauseadditionally deniesWAITFOR, the time-based blind-injection vector, andSQLExpressionValidatornow 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_queryincluded — these objects sit outside MemberJunction’s entity-permission model, so permitting them turned a validatedSELECTinto 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 ofExtraFilteris unaffected. - SQL literal-stripper bypass closed (
@memberjunction/global,@memberjunction/core): both of MJ’s SQL screens —ValidateUserProvidedSQLClause(guardingExtraFilter,OrderByandUserSearchString) andSQLExpressionValidator(guardingAggregatesand 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 likex = '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 singleStripSQLStringLiteralshelper implementing SQL-standard doubled-quote ('') semantics. - Values parameterized and escaped in SQL text-building paths (
@memberjunction/server,@memberjunction/core,@memberjunction/generic-database-provider):ReportResolver.CreateReportFromConversationDetailIDnow bindsConversationDetailIDthrough a parameterizedmssqlrequest as aUniqueIdentifierrather than interpolating it into the query string.GenericDatabaseProvider.CheckRecordRLSescapes embedded single quotes in primary-key values before building itsWHEREclause, mirroring the escaping already present in theLoad()path.RowLevelSecurityFilterInfo.MarkupFilterTextescapes embedded single quotes in substituted user-property values and treatsundefinedthe same asnull/object, leaving the token unresolved instead of substituting the literal string"undefined". - Authentication hardening (
@memberjunction/server,@memberjunction/ai-mcp-server): the superadminMJ_API_KEYcomparison ingetUserPayloadwas 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 withtimingSafeEqual. 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 againstalg=noneand RS256-to-HS256 confusion. - OAuth callback handler hardened (
@memberjunction/server): caller-suppliedconnectionIdwas interpolated into a rawExtraFilterwithout escaping and is now validated as a UUID at the request boundary and escaped at the SQL sink.frontendReturnUrlwas redirected to after only a URL-parse check, making the callback an open redirect from the trusted MJAPI origin; its origin is now validated againstcors.allowedOriginsplus 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):ValidateKeyByHashnow 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.
Bug Fixes
Section titled “Bug Fixes”- Action Filter evaluation implemented (
@memberjunction/actions):ActionEngineServer.RunSingleFilterwas areturn truestub, which made every Action Filter a no-op. Filters now resolve via a registeredBaseActionFiltersubclass (throughClassFactory, keyed by filter ID) or by evaluating the filter’sCodecolumn with anActionFilterContext, 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 containsActionFilterrows, so no existing behavior changes.
Upgrade Notes
Section titled “Upgrade Notes”userHandling.newUserAuthorizedDomainsnow matches email domains, not request origins. The new-user domain gate previously authorized against the hostname parsed from the request’sOriginheader, 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 forgingOrigin. 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.commatchesmail.example.combut notexample.com— list both if you need both; and identity providers that issue a bare username with noemailclaim can no longer auto-provision, with the denial logged explicitly. Relatedly, MCP OAuth auto-provisioning previously also required a non-empty requestOriginto enter the check at all, andMCPServer’sresolveOAuthUserpasses 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 narrowedcors.allowedOriginsare mostly self-protecting, since a browser frontend must already be on that list to call/oauth/initiateat 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 foreclosesalg=noneand RS256-to-HS256 confusion. - Negation-form RLS filters (
<>,NOT IN,NOT LIKE) now match fewer rows when the substituted user property isundefined. 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.