v6.1.6: SQL screening, action authorization and template sandbox security fixes
A security patch for the 6.1 line: caller SQL and filter screening, per-action RunAction authorization, sandboxed templates, and record names that respect field- and row-level security. Search previews now show record and entity display names, and AI-provider, cache and realtime recording fixes are included. No migrations.
Upgrade Notes apply: several requests 6.1.5 accepted are now refused, non-Owner users need Action Authorizations to run actions, and SQL features need the read-only database login.
MemberJunction 6.x is released under the Business Source License 1.1; 5.x and earlier stay ISC. What the licence grants, and what needs certification, is in the licence section of the v6 upgrade guide.
Improvements
Section titled “Improvements”- Search previews show record and entity display names (
@memberjunction/search-engine,@memberjunction/server,@memberjunction/graphql-dataprovider,@memberjunction/ng-search): preview autocomplete resolves each top result’s record name before it renders, replacing placeholder titles such asContacts Record. Search results now carryEntityDisplayName, and the preview, result cards, detail views and filter labels show it instead of the full schema-qualified entity name. - A run can be restricted to the caller’s own AI credentials (
@memberjunction/ai,@memberjunction/ai-core-plus,@memberjunction/ai-prompts,@memberjunction/ai-agents,@memberjunction/actions-base,@memberjunction/core-actions):ExecuteAgentParamsandAIPromptParamstakeCredentialScope: 'Any' | 'RuntimeOnly'. Under'RuntimeOnly'onlyapiKeysand a per-requestcredentialIdare used, so failover stays on vendors the caller keyed and a run they do not cover fails instead of finishing on the platform’s keys. Previously, when a customer’s Google key was rejected, failover could move to Vertex and complete the run on the platform’s account while reporting success. Omitting the parameter keeps the existing behaviour. Agents carry the scope to every prompt, sub-agent and internal sub-call (JSON repair, compaction, naming). - Custom semantic validators no longer need deep imports (
@memberjunction/react-linter):SemanticValidator,SemanticValidatorRegistryand their context and metadata types are exported from the package root. A validator can also opt in to checking a prop the parent does not pass by overridingEvaluatesAbsentPropto returntrue; it is then called withcontext.PropertyPresent === false. Validators that do not opt in run only for passed props, as before.
Bug Fixes
Section titled “Bug Fixes”- Data-layer injection and read-authorization gaps closed (
@memberjunction/core,@memberjunction/server,@memberjunction/sqlserver-dataprovider,@memberjunction/sql-dialect,@memberjunction/codegen-lib): composite-key values are escaped or validated before they reach record-name, primary-key and dependency SQL.ExecuteAdhocQueryauthorizes the rendered SQL: every table it reads must be an entity base view the caller can read in full.GetDatavalidates resolved SQL as a single read. SQL Server identifier quoting and REST paging parameters are hardened, and CodeGen writes descriptions as JSON strings so a description cannot break the generated code. - Template rendering is sandboxed, and on-demand template runs are authorized (
@memberjunction/templates,@memberjunction/server): a template could reachFunction,processand the shared rendering environment, and use them to run code on the server or alter later renders. Rendering now checks each template’s parse tree before it compiles and blocks those objects while it renders; a template that breaks a rule fails withTemplateSandboxError.RunTemplateandTemplate.Runrefuse scope-limited sessions and callers without Read onMJ: TemplatesandMJ: Template Contents. RunActionauthorizes each action, and its actions no longer run caller code (@memberjunction/server,@memberjunction/actions,@memberjunction/core-actions,@memberjunction/database-designer-actions): the mutation now checks each action. An Owner may run any action; any other user needs a role holding an authorization linked to the action throughMJ: Action Authorizations, unless the action checks the caller itself. Scope-limited sessions are refused.Calculate Expressionevaluates with an allowlisted math parser instead ofnew Function,ConditionalusesSafeExpressionEvaluator, andRun Ad-hoc Queryapplies the same checks asExecuteAdhocQuery.- The realtime relay no longer takes authority from a session’s editable
Config(@memberjunction/server,@memberjunction/ai-agents): a session owner could editConfigthrough the generic entity API to change the actions and run ids the relay trusted. Every relayed tool call now re-checksCanRunon the target agent, direct actions come from the session’s own projection or the agent’s metadata, and run ids inConfigare used only when they belong to the session. - RunView aggregate expressions cannot carry stacked SQL (
@memberjunction/sql-dialect,@memberjunction/sql-parser,@memberjunction/generic-database-provider,@memberjunction/server): the provider ran a caller’sAggregates[].expressiontext as given. Each expression must now be a single aggregate call over the entity’s columns, and the query runs SQL rebuilt from the checked parse tree. A refused expression returns a per-aggregate error; the rows and other aggregates still load. - Record-name lookups bind their keys and respect field- and row-level security (
@memberjunction/core,@memberjunction/server,@memberjunction/graphql-dataprovider): key values are bound as SQL parameters and only primary-key fields are accepted as key names. A record name built from a field the caller cannot read is withheld on every server lookup, including search results, and a record outside the caller’s read row filter shows its key instead of its name. Servers no longer keep a record-name cache shared across users, which could serve one user’s permitted name to another; the client-side cache that drives Explorer tab titles and breadcrumbs is unchanged. - Dataset reads apply the caller’s permissions (
@memberjunction/generic-database-provider,@memberjunction/server):GetDatasetByNameandGetDatasetStatusByNamescreen everyItemFilters[].Filterlike a RunViewExtraFilter, and the provider requires read permission on each item’s entity and applies the caller’s read row filter. - SQL data context items are gated like ad-hoc SQL (
@memberjunction/server,@memberjunction/data-context-server,@memberjunction/core-entities-server): a data context’s SQL items ran caller-written SQL unchecked. They now run only for the context’s owner or an Owner-type user, only on the read-only login, and only when the SQL is a single read over entity base views the caller can read in full. A refused item fails on its own and the other items still load. Only Owner-type users can save an item that holds SQL, and updates to these items always start from the stored row rather than client-supplied old values. - Every client RunView clause path is screened (
@memberjunction/generic-database-provider,@memberjunction/server,@memberjunction/lists,@memberjunction/core-entities-server,@memberjunction/ng-conversations,@memberjunction/sql-parser,@memberjunction/sql-dialect): the base-view clause screen covered four GraphQL resolvers. It now also guards batched cached RunViews, ad-hoc list sources, the REST view routes, and a saved view’s stored filter and sort. A subquery over an entity the user reads under a row filter or with denied fields is refused, because the subquery would read the base view without them. The conversations Tasks tab and task search now read the IDs they filter by with their own RunView, so they keep working under the screen. - A client filter cannot close the server’s parentheses (
@memberjunction/sql-parser,@memberjunction/generic-database-provider,@memberjunction/server): a fragment such as1=1) OR (1=1could take row-level security and tenant filters out of force.ExtraFilter,OverrideExcludeFilterand stored view filters must now each be one balanced expression, read with the dialect’s lexer, before they are combined with server-side filters. - SQL screens read quotes and comments the way the database does (
@memberjunction/global,@memberjunction/core,@memberjunction/generic-database-provider,@memberjunction/server,@memberjunction/sqlserver-dataprovider): a quote inside a bracket identifier, a comment or a PostgreSQL string could open a literal the database never saw, hiding a stacked statement from the validators. A dialect-aware scanner,ScanSQLForScreening, now classifies the text and rejects anything it cannot read. - Row-filtered subscribers no longer receive the keys of records they cannot read (
@memberjunction/server): a session whose every read of an entity is row-filtered now gets the entity, action and time oncacheInvalidation, with no key or row data. - MCP sign-in pins JWT algorithms, and the integration run reader blocks path traversal (
@memberjunction/ai-mcp-server,@memberjunction/integration-progress-artifacts): the Azure AD v1 verification path now uses the same algorithm allow-list as its sibling. Run IDs that contain separators or..are rejected, so the run queries can no longer probe files outside the run-log directory. - File and RSS actions are guarded against SSRF, and three server filters escape their literals (
@memberjunction/core-actions,@memberjunction/server): file-handling actions that load fromFileURLand Read RSS Feed now useSafeFetch, which blocks private, loopback, link-local and cloud-metadata targets and re-checks every redirect. The query duplicate-name check, entity vector document lookup and the new-user template’s email lookup now escape caller values. - Crafted text can no longer make AI error handling or chat mention parsing take quadratic time (
@memberjunction/ai,@memberjunction/ai-prompts,@memberjunction/conversations-runtime): six regular expressions inErrorAnalyzerandAIPromptRunner, and two inMentionParser, are replaced with linear scans. An unclosed@{earlier in a message no longer swallows a valid mention after it. - IS-A promotion over GraphQL no longer inserts a duplicate parent (
@memberjunction/graphql-dataprovider,@memberjunction/server,@memberjunction/ng-base-forms): giving an existing parent record a subtype from the browser created a second copy of the parent row, and a whole-chain create could leave the client holding a key that was never written. The client now sends the shared key and the server attaches the child to the existing parent. The IS-A related-records panel also refreshes when the form leaves edit mode, so a record created together with its subtype shows it without a manual refresh. - Rejected Google keys and failed streams report the real error (
@memberjunction/ai,@memberjunction/ai-prompts): invalid and expired Google API keys are classified asAuthentication, so the run stops instead of failing over on a dead key. A failed streaming call keeps its driver’s error classification and message instead of being recorded as “Unknown error”. - A dropped provider connection is classified as a network error (
@memberjunction/ai,@memberjunction/ai-gemini):ErrorAnalyzernow reads Node and undici connection codes from the error’scause, soNetworkOnlyfailover triggers on a real connection failure. - A cancelled prompt stops instead of failing over (
@memberjunction/ai-prompts,@memberjunction/ai-agents,@memberjunction/ai-bedrock): cancelling no longer walks every model candidate. Prompt runs are savedCancelledand agent runs endCancelledrather thanFailed. Failover attempt counts are saved correctly, and Bedrock streaming calls no longer fail with “stream is not async iterable”. - Gemini and Vertex honour
effortLevel, and agent responses no longer stick at “In-Progress” (@memberjunction/ai-gemini,@memberjunction/ai-agents): the thinking setting now reaches Google on every request, and thinking tokens count toward completion tokens and cost. A race between progress saves and the final update left some conversation responses showing “In-Progress” after the run finished. - Gemini 3.6 and later no longer receive sampling fields (
@memberjunction/ai-gemini,@memberjunction/ai-vertex):temperature,topPandtopKare dropped for models that reject them. Earlier models now actually receivetopPandtopK, and an explicittemperature: 0is sent as 0. - Grok Build 0.1 accepts prompts that set an effort level (
@memberjunction/ai-xai): xAI rejectsreasoning_effortfor this model, and every such run failed. The model’s x.ai rows no longer advertise effort-level support, so the runner drops it. complete_taskkeeps a finished answer whose payload contains stray escapes (@memberjunction/ai-agents): a model that wrote part ofpayloadChangeRequestwith\nescapes outside string values had the whole payload rejected, which could fail a run with a correct answer in hand. The argument is now repaired and parsed again, and a payload that still fails reports the parser’s error in the retry message.- Servers sharing Redis stop evicting each other’s cache in a loop (
@memberjunction/core,@memberjunction/redis-provider,@memberjunction/generic-database-provider,@memberjunction/sqlserver-dataprovider): during a rolling deploy, servers that disagreed on an entity’s fields deleted each other’s cache slots repeatedly. A schema-hash mismatch on a shared store is now a local miss. The SQL Server batched query cache-status check readsTotalRows, and query cache permission checks use the provider that owns the query. - Saves reach cache slots other servers wrote to Redis (
@memberjunction/core): a server invalidated only the slots in its own index, so peers could serve old rows until restart. - MJAPI survives a Redis outage (
@memberjunction/redis-provider,@memberjunction/core): the client stopped reconnecting after about 11 seconds and stayed cache-blind without logging. It now reconnects indefinitely with capped backoff, fails reads and writes fast while disconnected, logs connection loss in production, and on reconnect keeps or flushes the local cache depending on whether anything was invalidated during the gap. - Explorer no longer hides a newly opened record form (
@memberjunction/ng-explorer-core): a URL sync for the previous page could switch back to the navigation tab after a record tab had activated, leaving the form hidden. - Explorer no longer loads hidden record tabs at boot (
@memberjunction/ng-explorer-core): every record tab restored at boot loaded its content while the records region was still hidden, and the active tab’s reads waited behind them. An open AI Agent Run tab fetched its whole run tree before the user looked at it. Those tabs now load when the records region is shown. - Auth-flow and SyncData transactions no longer run on the shared provider (
@memberjunction/server): a transaction opened outside a GraphQL request nested into every concurrent caller’s, and one left open swallowed every later write on the global provider (scheduled jobs, REST extensions, agent logging) until restart. New-user creation, magic-link provisioning and SyncData’s delete-with-filter now each run on their own provider, exported asCreateIsolatedProvider(), and the delete-with-filter deletes now run inside their transaction. - Vectorization into a colocated vector database works when the syncer has no provider (
@memberjunction/ai-vectors,@memberjunction/server,@memberjunction/ai-agents,@memberjunction/core-actions,@memberjunction/actions-content-autotag,@memberjunction/ai-knowledge-pipeline): with SQL Server 2025VECTORor colocated pgvector, every upsert failed with “requires a host connection” because the fallback provider could not host the vectors. The fallback now usesMetadata.Provider, and the resolver, Knowledge Agent, vectorize and autotag actions, and the knowledge pipeline pass the provider they already hold. - Create row-level-security filters can reference NULL columns (
@memberjunction/generic-database-provider): the Create check left NULL fields out of the row it tested, so a filter on a nullable column the new row left NULL failed withInvalid column nameinstead of evaluating. For example, saving a sharedMJ: Projectsfolder under anOwnerUserIDfilter raised a SQL error rather than “Access denied”, and anOwnerUserID IS NULLbranch could never pass. - CodeGen records every table-level validator (
@memberjunction/codegen-lib): only the first table-level CHECK-constraint validator of each entity was saved, so later ones were regenerated by the LLM with new names and wording on every database run, and--skipdbdropped them. mj app upgraderefreshes the app’s schema name from its manifest (@memberjunction/open-app-engine): a corrected schema name, in practice a casing fix, was only written at install. The stale value fed GraphQL type names, and every operation on the app’s entities failed withGRAPHQL_VALIDATION_FAILED.mj sync pullbacks up every file it rewrites and rolls back correctly (@memberjunction/metadata-sync): files that only gained records were not backed up, so a failed pull left them half-written. A backup that cannot be written now stops the pull, and pull warns when an entity’s subclass is not registered.mj sync pushreports real changes after a pull and in dry runs (@memberjunction/metadata-sync): pull and push hashed records differently, so the first push after a pull rewrote every record, and--dry-runreported every existing record as an update.- Role row-level-security filters with subqueries can be edited again (
@memberjunction/core-entities-server,@memberjunction/integration-test-suite): a filter referenced by role permission rows was checked with the API-key filter rules, which refused subqueries and filters shared across entities. Such filters are now screened for dangerous SQL and compiled against every referencing entity’s base view. - The installer’s smoke-test cleanup no longer kills the installer (
@memberjunction/installer): on macOS and Linux,mj installcould exit 137 and leave Explorer running. - Box storage renews client-credentials tokens (
@memberjunction/storage): a driver reused for more than about an hour failed every read withCannot refresh Box token: missing credentials. - Duplicate detection finishes on large entities and matches what vector sync stored (
@memberjunction/ai-vector-dupe,@memberjunction/ai-vector-sync,@memberjunction/core,@memberjunction/ai-agents): detection held every result in memory and exhausted the heap on large entities, and checked only the first 1,000 records. Its query text now renders related-entity template data the same way sync does, and an entity document’srecordFilter.extraFilter, which nothing read before, now applies to both sync and detection. - Realtime sessions keep their tools, and durable flows get data and storable outputs (
@memberjunction/ai-realtime-client,@memberjunction/task-graph,@memberjunction/core-actions,@memberjunction/server): OpenAI-protocol sessions rejected the session update and ran without instructions or tools. Dispatched workflow steps resolvedata.*andcontext.*mappings instead of passing them as literal text, and durable Execute Agent steps return serializable results. - Realtime recordings include the agent’s voice on Gemini and other PCM-playback drivers (
@memberjunction/ai,@memberjunction/ai-realtime-client,@memberjunction/ng-conversations): agent audio from these drivers is now mixed into the recording. If it cannot be mixed, microphone capture is kept, and recorder setup resources are released when recording stops.
Upgrade Notes
Section titled “Upgrade Notes”- Upgrade every MemberJunction package to 6.1.6 together. The client and server halves of several fixes must match. There are no migrations.
- Configure the read-only database login (
dbReadOnlyUsername/dbReadOnlyPassword, or the PostgreSQL read-only credentials). Ad-hoc SQL, the Run Ad-hoc Query action and SQL data context items now refuse to run without it. - Link actions to Action Authorizations before upgrading (
@memberjunction/server,@memberjunction/actions): non-Owner users can run an action throughRunActiononly when it is linked to an authorization one of their roles holds, or when the action checks the caller itself (Database Designer, interactive forms). Send To Audience, Run Integration Sync, Home pinned actions and the action test harness refuse non-Owners until linked.SkipActionLogis ignored. Calculate Expression accepts arithmetic only;%is now floored modulo, andlogandroundhonour their second argument. - Caller SQL and filters against raw tables are refused (
@memberjunction/server,@memberjunction/generic-database-provider): check custom clients and saved SQL. Every tableExecuteAdhocQueryreads must be an entity base view (vw…) the caller can read with no row filter or denied fields; CTEs are exempt.ExtraFilter,OrderByandOverrideExcludeFiltersubqueries, and non-custom saved views’ stored filters and sorts, must read entity base views the user can read. REST view routes no longer acceptViewEntity; name the view byViewIDorViewName. List refreshes and audience actions with such filters are refused. - Some RunView aggregate formulas are refused (
@memberjunction/generic-database-provider): arithmetic or functions around the aggregate call (SUM(Amount) + 1,ROUND(AVG(Price), 2)),CONVERT(useCAST), window clauses, PostgreSQLFILTERandEXTRACT,COUNT_BIG(*)andTRY_CASTreturn a per-aggregate error. - Client filters must be one balanced expression (
@memberjunction/sql-parser): on PostgreSQL, unquoted names with non-ASCII characters and dollar-quoted strings in filters are refused; quote the name. A few rare quoting forms are refused on paths that pass no dialect, such as[Today's Count]. - Template runs need permission (
@memberjunction/templates,@memberjunction/server):RunTemplateandTemplate.Runrefuse anonymous and resource-scoped magic-link sessions, widget guests, and callers without Read onMJ: TemplatesandMJ: Template Contents. A template that uses an inherited name (toString,constructor,prototype) as a dictionary key, keyword argument, macro argument, loop orsetvariable, or filter name fails withTemplateSandboxError. All shipped templates pass. - Record names follow field- and row-level security (
@memberjunction/core,@memberjunction/server): a record outside the caller’s read row filter shows its key instead of its name in foreign-key labels, breadcrumbs and pickers, and lookups by a non-key field are refused. On a server,GetCachedRecordNamewithoutloadIfNeedednow returnsundefined. - Datasets read as the caller (
@memberjunction/generic-database-provider): dataset items apply the caller’s entity permissions and row filters, andItemFiltersare screened likeExtraFilter. - SQL data context items (
@memberjunction/data-context-server): only the item’s owner or an Owner-type user can run them, and only Owner-type users can save an item that holds SQL. - Guest voice sessions no longer run actions directly (
@memberjunction/server): on the client-direct realtime path, magic-link sessions and public web-widget guests get no direct-action tools; the co-agent delegates throughinvoke-target-agent, which is slower for simple lookups. The LiveKit bridge path is unchanged. - Row-filtered users get table-level cache reloads on save (
@memberjunction/server): their client reloads the entity’s whole cache instead of one record. - IS-A create over GraphQL (
@memberjunction/graphql-dataprovider,@memberjunction/server): the client now sends the shared key for a new IS-A child and the server loads the parent under that key as the caller. Upgrade client and server together. - Google invalid or expired API keys stop the run (
@memberjunction/ai): they no longer fail over to other models. - Review effort levels on Gemini and Vertex prompts (
@memberjunction/ai-gemini): prompts, prompt-model rows and agents that set an effort level now think at that level, which changes token use and cost. Prompts with no effort level are unchanged. - Grok Build 0.1 is a metadata fix (
@memberjunction/ai-xai): runmj sync pushformetadata/ai-modelsto pick it up. - The first CodeGen run regenerates some validators once (
@memberjunction/codegen-lib): for entities with two or more table-level CHECK constraints, the first run after upgrading regenerates theirValidate*()methods and records them; later runs reproduce them.