{"id":"EEF-CVE-2026-120295","summary":"Incorrect authorization in the devices page bulk move allows view-role members to move devices into another organization and receive its firmware","details":"## Summary\n\nIncorrect Authorization vulnerability in the devices page bulk move in nerves-hub nerves_hub_web allows an authenticated user with the `manage` role in one organization and only the `view` role in another to move devices into the second organization's product via the \"Move device(s) to product\" action. The `target-product` event in `NervesHubWeb.Live.Devices.Index` looks the product up with `NervesHub.Products.get_by_id/2`, which checks that the user is a member of the product's organization at any role, and `move-devices-product` then changes each selected device's organization and product. The device API's move requires `manage` in the target organization, and an earlier fix added the check on the organization the devices leave but not on the one they enter.\n\nA moved device keeps its key and certificate, joins the target organization's matching deployment groups, and is sent their firmware with a download URL, which the `view` role cannot otherwise obtain. The user cannot change or control devices that already belong to the other organization.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3.\n\n## Details\n\n**1. Target selection.** The bulk actions sidebar (`NervesHubWeb.Components.BulkActionsSidebar`) lists the products of every organization in `current_scope.user.orgs`. Choosing one sends `target-product` to `NervesHubWeb.Live.Devices.Index`, which requires `device:update` in the current organization and then calls `NervesHub.Products.get_by_id/2`. That query joins the product's organization to its `org_users` rows and filters on the user's id only, so any membership in the target organization satisfies it, including the `view` role.\n\n**2. The move.** `move-devices-product` passes the selected devices, or every device matching the current filter, to `NervesHub.Devices.BulkActions.move_many/3`, which calls `Devices.move/3` for each one and rewrites its `org_id` and `product_id`. No check of the user's role in the target organization happens on this path, while the API's move in `NervesHubWeb.API.DeviceController` requires `manage` there.\n\n**3. Effect.** The moved device still authenticates with its own certificate. On its next connection it is matched against the target product's deployment groups by platform, architecture and tags, joins one, and is sent that group's firmware with a download URL.\n\n## Proof of concept\n\nAs a user with `manage` in organization A and `view` in organization B, where B has an active deployment group:\n\n1. On A's devices page, give a device you control the tags B's deployment group asks for. A `view` member can see the group's conditions.\n2. Select the device, choose B's product under \"Move device(s) to product\" and press Move. The page reports the move as successful, and the device is now in B's product.\n3. Connect the device, reporting the platform and architecture of the group's firmware. It joins the group and is sent B's firmware with a download URL.\n\n## Impact\n\nA user who manages devices in one organization and holds only the view role in another can move their own devices into the second organization. The devices join its matching deployment groups and receive its firmware, which the view role cannot otherwise download, and they report health, logs and alarms into that organization. Devices already belonging to the other organization are not affected.\n\n## Solutions\n\nEvery move is recorded in the target product's audit log as \"User \u003cname\u003e moved device \u003cidentifier\u003e to \u003corg\u003e : \u003cproduct\u003e\". After upgrading, review the `audit_logs` rows with resource type `NervesHub.Products.Product` and a description containing \"moved device\" whose actor does not hold the `manage` or `admin` role in that organization, and move or remove the devices they brought in. Roles may have changed since a move, and entries older than the retention period are gone when `TRUNCATE_AUDIT_LOGS_ENABLED` is on.","aliases":["CVE-2026-120295","GHSA-2j7r-q3vm-8j34"],"modified":"2026-11-10T10:56:15.333721823Z","published":"2026-11-10T10:43:26.342Z","database_specific":{"capec_ids":["CAPEC-122"],"cpe_ids":["cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"],"cwe_ids":["CWE-863"]},"references":[{"type":"ADVISORY","url":"https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-2j7r-q3vm-8j34"},{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-120295.html"},{"type":"WEB","url":"https://github.com/nerves-hub/nerves_hub_web/commit/adaeefdb7a835525482588f43332ef988cc448c7"},{"type":"WEB","url":"https://github.com/nerves-hub/nerves_hub_web/commit/79743ef08f7f801617b653fd09f37c01ed9b4be0"},{"type":"FIX","url":"https://github.com/nerves-hub/nerves_hub_web/commit/1bc8be875ab6b1eb904e5ff60c8a1421c5c700fa"}],"affected":[{"ranges":[{"type":"GIT","repo":"https://github.com/nerves-hub/nerves_hub_web","events":[{"introduced":"adaeefdb7a835525482588f43332ef988cc448c7"}]},{"type":"GIT","repo":"https://github.com/nerves-hub/nerves_hub_web","events":[{"introduced":"79743ef08f7f801617b653fd09f37c01ed9b4be0"},{"fixed":"1bc8be875ab6b1eb904e5ff60c8a1421c5c700fa"}]}],"versions":["v1.3.0","v1.2.0","v1.1.0","v1.0.4","v1.0.3","v1.0.2","v1.0.1","v1.0.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.0","v2.2.0","v2.1.0","v2.0.0"],"database_specific":{"source":"https://cna.erlef.org/osv/EEF-CVE-2026-120295.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N"}],"credits":[{"name":"arbor-s","type":"FINDER"},{"name":"arbor-s","type":"REPORTER"},{"name":"Josh Kalderimis","type":"REMEDIATION_DEVELOPER"},{"name":"Jonatan Männchen / EEF","type":"COORDINATOR"}]}