VYPR
High severity7.2NVD Advisory· Published May 26, 2026· Updated May 26, 2026

CVE-2026-44730

CVE-2026-44730

Description

OpenCTI is an open source platform for managing cyber threat intelligence knowledge and observables. Prior to 6.9.7, an organization admin can escalate their privileges by adding a user from a different organization with higher privileges, to their own organization. This is due to incorrect ACL on userEdit relationAdd. This vulnerability is fixed in 6.9.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Privilege escalation in OpenCTI < 6.9.7 allows org admin to add cross-org users with higher privileges via incorrect ACL on userEdit relationAdd.

Vulnerability

In OpenCTI versions prior to 6.9.7, the GraphQL mutation userEdit has an incorrect ACL on the relationAdd operation. This allows an organization administrator to add a user from a different organization to their own organization, even if that user holds higher privileges (e.g., platform administrator). The vulnerability arises because the access control check does not properly restrict the scope of users that can be added. [1]

Exploitation

An attacker must have organization admin privileges in one organization. The attacker can craft a GraphQL request using the userEdit mutation to add a privileged user from another organization (e.g., a platform admin) to their organization. No special user interaction is required, and the attack can be performed remotely. [1]

Impact

Successful exploitation results in privilege escalation: the attacker (org admin) effectively gains the privileges of the added user, which could be platform admin or another high-privilege role. This can lead to full compromise of the OpenCTI platform, including unauthorized access to all threat intelligence data, modification of knowledge, and disruption of services. [1]

Mitigation

The vulnerability is fixed in OpenCTI version 6.9.7. Users should upgrade to this version or later immediately. No workarounds are available for earlier versions. [1]

AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

2
bbd3bc60f014

[backend] users add relations in orga admin context (#13901)

https://github.com/OpenCTI-Platform/openctiArchidoitJan 8, 2026Fixed in 6.9.7via llm-release-walk
2 files changed · +65 40
  • opencti-platform/opencti-graphql/src/domain/user.js+42 38 modified
    @@ -184,7 +184,7 @@ const extractTokenFromBasicAuth = async (authorization) => {
     
     export const findById = async (context, user, userId) => {
       if (!isUserHasCapability(user, SETTINGS_SET_ACCESSES) && user.id !== userId) {
    -    // if no organization in common with the logged user
    +    // if no organization in common with the logged user administrated organizations
         const memberOrganizations = await fullEntitiesThroughRelationsToList(context, user, userId, RELATION_PARTICIPATE_TO, ENTITY_TYPE_IDENTITY_ORGANIZATION);
         const myOrganizationsIds = user.administrated_organizations.map((organization) => organization.id);
         if (!memberOrganizations.map((organization) => organization.id).find((orgaId) => myOrganizationsIds.includes(orgaId))) {
    @@ -525,17 +525,35 @@ const isUserAdministratingOrga = (user, organizationId) => {
       return user.administrated_organizations.some(({ id }) => id === organizationId);
     };
     
    +const loadUserToUpdateWithAccessCheck = async (context, user, userId) => {
    +  const userToUpdate = await internalLoadById(context, user, userId, { type: ENTITY_TYPE_USER });
    +  if (!userToUpdate) {
    +    throw FunctionalError(`${ENTITY_TYPE_USER} cannot be found.`, { userId });
    +  }
    +  if (!isUserHasCapability(user, SETTINGS_SET_ACCESSES) && user.id !== userId) {
    +    // Check in an organization admin edits a user that's not in its administrated organizations
    +    if (isOnlyOrgaAdmin(user)) {
    +      const myAdministratedOrganizationsIds = user.administrated_organizations.map((orga) => orga.id);
    +      if (!userToUpdate[RELATION_PARTICIPATE_TO]?.find((orga) => myAdministratedOrganizationsIds.includes(orga))) {
    +        throw ForbiddenAccess();
    +      }
    +    } else {
    +      throw ForbiddenAccess();
    +    }
    +  }
    +  return userToUpdate;
    +};
    +
     export const assignOrganizationToUser = async (context, user, userId, organizationId) => {
       if (isOnlyOrgaAdmin(user)) {
         // When user is organization admin, we make sure she is also admin of organization added
         if (!isUserAdministratingOrga(user, organizationId)) {
           throw ForbiddenAccess();
         }
       }
    -  const targetUser = await findById(context, user, userId);
    -  if (!targetUser) {
    -    throw FunctionalError('Cannot add the relation, User cannot be found.', { userId });
    -  }
    +  // check the user is accessible
    +  const targetUser = await loadUserToUpdateWithAccessCheck(context, user, userId);
    +
       const input = { fromId: userId, toId: organizationId, relationship_type: RELATION_PARTICIPATE_TO };
       const created = await createRelation(context, user, input);
       const actionEmail = ENABLED_DEMO_MODE ? REDACTED_USER.user_email : created.from.user_email;
    @@ -899,14 +917,7 @@ export const roleDeleteRelation = async (context, user, roleId, toId, relationsh
     // User related
     export const userEditField = async (context, user, userId, rawInputs) => {
       const inputs = [];
    -  const userToUpdate = await internalLoadById(context, user, userId);
    -  // Check in an organization admin edits a user that's not in its administrated organizations
    -  const myAdministratedOrganizationsIds = user.administrated_organizations.map((orga) => orga.id);
    -  if (isOnlyOrgaAdmin(user)) {
    -    if (userId !== user.id && !userToUpdate[RELATION_PARTICIPATE_TO].find((orga) => myAdministratedOrganizationsIds.includes(orga))) {
    -      throw ForbiddenAccess();
    -    }
    -  }
    +  const userToUpdate = await loadUserToUpdateWithAccessCheck(context, user, userId);
       let skipThisInput = false;
       for (let index = 0; index < rawInputs.length; index += 1) {
         const input = rawInputs[index];
    @@ -1186,14 +1197,9 @@ export const deleteAllNotificationByUser = async (userId) => {
      * @returns {Promise<*>}
      */
     export const userDelete = async (context, user, userId) => {
    -  if (isOnlyOrgaAdmin(user)) {
    -    // When user is organization admin, we make sure that the deleted user is in one of the administrated organizations of the admin
    -    const userData = await storeLoadById(context, user, userId, ENTITY_TYPE_USER);
    -    const myAdministratedOrganizationsIds = user.administrated_organizations.map(({ id }) => id);
    -    if (!userData[RELATION_PARTICIPATE_TO].find((orga) => myAdministratedOrganizationsIds.includes(orga))) {
    -      throw ForbiddenAccess();
    -    }
    -  }
    +  // check rights
    +  await loadUserToUpdateWithAccessCheck(context, user, userId);
    +
       await deleteAllTriggerAndDigestByUser(userId);
       await deleteAllNotificationByUser(userId);
       await deleteAllWorkspaceForUser(context, user, userId);
    @@ -1213,14 +1219,14 @@ export const userDelete = async (context, user, userId) => {
     };
     
     export const userAddRelation = async (context, user, userId, input) => {
    -  const userData = await storeLoadById(context, user, userId, ENTITY_TYPE_USER);
    -  if (!userData) {
    -    throw FunctionalError(`Cannot add the relation, ${ENTITY_TYPE_USER} cannot be found.`, { userId });
    -  }
    +  // check the user is accessible
    +  const userData = await loadUserToUpdateWithAccessCheck(context, user, userId);
    +
    +  // check the relationship type
       if (!isInternalRelationship(input.relationship_type)) {
         throw FunctionalError(`Only ${ABSTRACT_INTERNAL_RELATIONSHIP} can be added through this method, got ${input.relationship_type}.`);
       }
    -  // Check in case organization admins adds non-grantable goup a user
    +  // Check in case organization admins adds non-grantable group a user
       const myGrantableGroups = R.uniq(user.administrated_organizations.map((orga) => orga.grantable_groups).flat());
       if (isOnlyOrgaAdmin(user)) {
         if (input.relationship_type === 'member-of' && !myGrantableGroups.includes(input.toId)) {
    @@ -1260,10 +1266,9 @@ export const userDeleteRelation = async (context, user, targetUser, toId, relati
     };
     
     export const userIdDeleteRelation = async (context, user, userId, toId, relationshipType) => {
    -  const userData = await storeLoadById(context, user, userId, ENTITY_TYPE_USER);
    -  if (!userData) {
    -    throw FunctionalError('Cannot delete the relation, User cannot be found.', { userId });
    -  }
    +  // check the user is accessible
    +  const userData = await loadUserToUpdateWithAccessCheck(context, user, userId);
    +
       if (!isInternalRelationship(relationshipType)) {
         throw FunctionalError(`Only ${ABSTRACT_INTERNAL_RELATIONSHIP} can be deleted through this method, got ${relationshipType}.`);
       }
    @@ -1277,10 +1282,8 @@ export const userDeleteOrganizationRelation = async (context, user, userId, toId
           throw ForbiddenAccess();
         }
       }
    -  const targetUser = await findById(context, user, userId);
    -  if (!targetUser) {
    -    throw FunctionalError('Cannot delete the relation, User cannot be found.', { userId });
    -  }
    +  // check the user is accessible
    +  const targetUser = await loadUserToUpdateWithAccessCheck(context, user, userId);
     
       const { to } = await deleteRelationsByFromAndTo(context, user, userId, toId, RELATION_PARTICIPATE_TO, ABSTRACT_INTERNAL_RELATIONSHIP);
       if (to.authorized_authorities?.includes(userId)) {
    @@ -1690,10 +1693,9 @@ export const userRenewToken = async (context, user, userId) => {
         throw FunctionalError('Cannot renew token of admin user defined in configuration, please change configuration instead.');
       }
     
    -  const userData = await storeLoadById(context, user, userId, ENTITY_TYPE_USER);
    -  if (!userData) {
    -    throw FunctionalError(`Cannot renew token, ${userId} user cannot be found.`);
    -  }
    +  // check the user is accessible
    +  const userData = await loadUserToUpdateWithAccessCheck(context, user, userId);
    +
       const patch = { api_token: uuid() };
       const { element } = await patchAttribute(context, user, userId, ENTITY_TYPE_USER, patch);
     
    @@ -1872,11 +1874,13 @@ export const findDefaultDashboards = async (context, user, currentUser) => {
     
     // region context
     export const userCleanContext = async (context, user, userId) => {
    +  await loadUserToUpdateWithAccessCheck(context, user, userId);
       await delEditContext(user, userId);
       return storeLoadById(context, user, userId, ENTITY_TYPE_USER);
     };
     
     export const userEditContext = async (context, user, userId, input) => {
    +  await loadUserToUpdateWithAccessCheck(context, user, userId);
       await setEditContext(user, userId, input);
       return storeLoadById(context, user, userId, ENTITY_TYPE_USER);
     };
    
  • opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/user-test.ts+23 2 modified
    @@ -1,5 +1,5 @@
     import gql from 'graphql-tag';
    -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
    +import { afterAll, beforeAll, describe, expect, it } from 'vitest';
     import { elLoadById } from '../../../src/database/engine';
     import { generateStandardId } from '../../../src/schema/identifier';
     import { ENTITY_TYPE_CAPABILITY, ENTITY_TYPE_GROUP, ENTITY_TYPE_USER } from '../../../src/schema/internalObject';
    @@ -20,6 +20,7 @@ import {
       USER_CONNECTOR,
       USER_DISINFORMATION_ANALYST,
       USER_EDITOR,
    +  USER_SECURITY,
     } from '../../utils/testQuery';
     import { ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../../../src/modules/organization/organization-types';
     import { VIRTUAL_ORGANIZATION_ADMIN } from '../../../src/utils/access';
    @@ -37,7 +38,6 @@ import { OPENCTI_ADMIN_UUID } from '../../../src/schema/general';
     import type { Capability, Member, UserAddInput } from '../../../src/generated/graphql';
     import { storeLoadById } from '../../../src/database/middleware-loader';
     import { entitiesCounter } from '../../02-dataInjection/01-dataCount/entityCountHelper';
    -import * as entrepriseEdition from '../../../src/enterprise-edition/ee';
     
     const LIST_QUERY = gql`
       query users(
    @@ -980,6 +980,18 @@ describe('User has no settings capability and is organization admin query behavi
         });
         expect(queryResult.data.userEdit.fieldPatch.account_status).toEqual('Inactive');
       });
    +  it('should not update user with no organization', async () => {
    +    await queryAsUserIsExpectedForbidden(USER_EDITOR.client, {
    +      query: UPDATE_QUERY,
    +      variables: { id: ADMIN_USER.id, input: { key: 'account_status', value: ['Inactive'] } },
    +    });
    +  });
    +  it('should not update user from an other organization', async () => {
    +    await queryAsUserIsExpectedForbidden(USER_EDITOR.client, {
    +      query: UPDATE_QUERY,
    +      variables: { id: USER_SECURITY.id, input: { key: 'account_status', value: ['Inactive'] } },
    +    });
    +  });
       it('should not add organization to user if not admin', async () => {
         platformOrganizationId = await getOrganizationIdByName(PLATFORM_ORGANIZATION.name);
         await queryAsUserIsExpectedForbidden(USER_EDITOR.client, {
    @@ -990,6 +1002,15 @@ describe('User has no settings capability and is organization admin query behavi
           },
         });
       });
    +  it('should not add organization to user if user is not in its own organization', async () => {
    +    await queryAsUserIsExpectedForbidden(USER_EDITOR.client, {
    +      query: ORGANIZATION_ADD_QUERY,
    +      variables: {
    +        id: ADMIN_USER.id,
    +        organizationId: testOrganizationId,
    +      },
    +    });
    +  });
       it('should administrate more than 1 organization', async () => {
         // Need to add granted_groups to PLATFORM_ORGANIZATION because of line 533 in domain/user.js
         const grantableGroupQueryResult = await adminQuery({
    
97ca75ecb6b1

[backend/worker] Release 6.9.7

https://github.com/OpenCTI-Platform/openctiFiligran AutomationJan 12, 2026Fixed in 6.9.7via release-tag
5 files changed · +5 5
  • client-python/pycti/__init__.py+1 1 modified
    @@ -1,5 +1,5 @@
     # -*- coding: utf-8 -*-
    -__version__ = "6.9.6"
    +__version__ = "6.9.7"
     
     from .api.opencti_api_client import OpenCTIApiClient
     from .api.opencti_api_connector import OpenCTIApiConnector
    
  • opencti-platform/opencti-front/package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "opencti-front",
    -  "version": "6.9.6",
    +  "version": "6.9.7",
       "private": true,
       "workspaces": [
         "packages/*"
    
  • opencti-platform/opencti-graphql/package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "opencti-graphql",
    -  "version": "6.9.6",
    +  "version": "6.9.7",
       "private": true,
       "scripts": {
         "check-ts": "tsc --noEmit",
    
  • opencti-platform/opencti-graphql/src/python/requirements.txt+1 1 modified
    @@ -1,4 +1,4 @@
    -pycti==6.9.6
    +pycti==6.9.7
     parsuricata==0.4.1
     yara-python==4.5.2
     sigmatools==0.23.1
    
  • opencti-worker/src/requirements.txt+1 1 modified
    @@ -1,4 +1,4 @@
    -pycti==6.9.6
    +pycti==6.9.7
     opentelemetry-api~=1.35.0
     opentelemetry-sdk~=1.35.0
     opentelemetry-exporter-prometheus==0.56b0
    

Vulnerability mechanics

Root cause

"Missing access control check in userAddRelation and other user mutation endpoints allows an organization admin to operate on users outside their administrated organizations."

Attack vector

An attacker who is an organization admin (but not a global settings admin) can call userAddRelation, userEditField, userDelete, assignOrganizationToUser, userIdDeleteRelation, userDeleteOrganizationRelation, userRenewToken, userCleanContext, or userEditContext with a userId that belongs to a user from a different organization. Because the original code did not verify that the target user belongs to one of the attacker's administrated organizations, the attacker could add that user to their own organization, thereby inheriting the user's higher privileges. The attack requires network access to the GraphQL API and valid credentials for an organization-admin account [patch_id=2566839].

Affected code

The vulnerability affects multiple endpoints in `opencti-platform/opencti-graphql/src/domain/user.js`: `userAddRelation`, `userEditField`, `userDelete`, `assignOrganizationToUser`, `userIdDeleteRelation`, `userDeleteOrganizationRelation`, `userRenewToken`, `userCleanContext`, and `userEditContext`. These functions previously lacked or had inconsistent access checks for organization-admin users operating on users outside their administrated organizations [patch_id=2566839].

What the fix does

The patch introduces a new helper function `loadUserToUpdateWithAccessCheck` that loads the target user and then, for organization-admin users, verifies that the target user belongs to at least one of the admin's administrated organizations. If not, it throws `ForbiddenAccess`. This function is then called at the beginning of `userAddRelation`, `userEditField`, `userDelete`, `assignOrganizationToUser`, `userIdDeleteRelation`, `userDeleteOrganizationRelation`, `userRenewToken`, `userCleanContext`, and `userEditContext`, replacing the previous ad-hoc or missing checks [patch_id=2566839]. The fix ensures that an organization admin can only modify users who are members of organizations they administrate.

Preconditions

  • authAttacker must have valid credentials for an organization-admin account (isOnlyOrgaAdmin returns true).
  • networkAttacker must have network access to the OpenCTI GraphQL API endpoint.
  • inputAttacker must know or guess the userId of a target user from a different organization.

Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.