GitHub Advisoryadvisory · npm

GHSA-5ffh-6f9q-5hhr Unleash: A project member can reorder activation strategies belonging to any other project / environment (cross-project integrity write), bypassing project RBAC and the audit log

MEDIUM Published 2026-09-22 Modified 2026-09-22 Official page ↗
Aliases
CVE-2026-77425
Ecosystem
npm
Packages
unleash-server
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Weaknesses
CWE-639, CWE-863
Reviewed
2026-09-22

Affected packages

• npm unleash-server: < 8.0.3 → fixed in 8.0.3

Description

Summary Unleash scopes write permissions per project and per environment: a user with the `UPDATE_FEATURE_STRATEGY` permission on project `A` is supposed to be able to mutate activation strategies only within project `A`. The endpoint `POST /api/admin/projects/:projectId/features/:featureName/environments/:environment/strategies/set-sort-order` violates this. The RBAC middleware authorizes the request against the `:projectId` taken from the URL, but the handler then writes the strategy IDs supplied in the request *body* directly to the database by primary key, without ever verifying that those strategy IDs actually belong to the URL's project / feature / environment. A low-privilege member of any one project can therefore reorder the activation strategies of features in *any other project and environment* — including projects they have no role on at all — by putting their own project in the URL (to satisfy RBAC) and the victim project's strategy IDs in the body. The sibling write paths in the same service (`updateStrategy`, `patchStrategy`, `deleteStrategy`) all call `validateUpdatedProperties()`, which rejects a strategy whose stored `projectId`/`featureName` does not match the URL context. The `set-sort-order` handler is the one sibling that omits this check — an asymmetric, incomplete enforcement. Activation-strategy ordering is security-relevant: the first matching strategy determines a flag's rollout/variant outcome, so an attacker can flip which strategy "wins" for another team's feature flag in production. As a secondary effect, the operation that mutates the victim's strategies is recorded (if at all) under the *attacker's* project/feature context, so the tampering does not appear in the victim project's audit trail. Affected code (v8.0.0) The route is registered with the project-scoped permission `UPDATE_FEATURE_STRATEGY` (correct), and the handler forwards the URL params as the "context" plus the raw request body: `src/lib/features/feature-toggle/feature-toggle-controller.ts` ```ts { method: 'post', path: `${PATH_STRATEGIES}/set-sort-order`, handler: this.setStrategiesSortOrder, permission: UPDATE_FEATURE_STRATEGY, // ... } async setStrategiesSortOrder(req, res): Promise { const { featureName, projectId, environment } = req.params; await this.transactionalFeatureToggleService.transactional((service) => service.updateStrategiesSortOrder( { featureName, environment, projectId }, // URL context only req.body, // attacker-controlled [{id, sortOrder}] req.audit, ), ); res.status(200).send(); } ``` The service writes each body-supplied `id` directly. It reads the URL-context strategies only to build the audit-event payload (`existingOrder`/`newOrder`); it never validates that the IDs in `sortOrders` belong to that context: `src/lib/features/feature-toggle/feature-toggle-service.ts` ```ts async unprotectedUpdateStrategiesSortOrder(context, sortOrders, auditUser): Promise> { const { featureName, environment, projectId: project } = context; const existingOrder = (await this.getStrategiesForEnvironment(project, featureName, environment)) .sort(sortStrategies).map((s) => s.id); // ... await Promise.all( sortOrders.map(({ id, sortOrder }) => this.featureStrategiesStore.updateSortOrder(id, sortOrder), // NO project/feature/env check ), ); // ...event built from the URL context, not from the strategies actually mutated... } ``` The store updates by primary key with no scoping predicate: `src/lib/features/feature-toggle/feature-toggle-strategies-store.ts` ```ts async updateSortOrder(id: string, sortOrder: number): Promise { await this.db(T.featureStrategies) .where({ id }) .update({ sort_order: sortOrder }); } ``` Contrast the sibling mutators, which DO bind the target strategy to the URL context (`validateUpdatedProperties` throws `InvalidOperationError` when `existingStrategy.projectId !== projectId` or `existingStrategy.featureName !== featureName`): ```ts // unprotectedUpdateStrategy / patchStrategy / deleteStrategy: const existingStrategy = await this.featureStrategiesStore.get(id); this.validateUpdatedProperties(context, existingStrategy); // 1 project are Pro/Enterprise features; the official OSS image hard-pins requests to the `default` project via an unrelated edition gate (`resolveIsOss`). To reproduce the cross-project dimension on the public image, lift only that edition gate (this does NOT touch the vulnerable `set-sort-order` code path). On a real Pro/Enterprise instance this step is unnecessary — multiple projects already exist. ```b…

Related

Alias · CVE (1)

  • CVE-2026-77425 Unleash is an open-source feature management platform. vulnerability

Weakness · CWE (2)

  • CWE-639 Authorization Bypass Through User-Controlled Key weakness
  • CWE-863 Incorrect Authorization weakness

References