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
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