Search

Find a vulnerability

Search criteria

    8 vulnerabilities by nerves-hub

    CVE-2026-119446 (GCVE-0-2026-119446)

    Vulnerability from cvelistv5 – Published: 2026-11-10 10:44 – Updated: 2026-11-10 10:44
    VLAI
    Title
    Session fixation in the CLI login flow allows an unauthenticated attacker to obtain a user's API token
    Summary
    Session Fixation vulnerability in the CLI login flow in nerves-hub nerves_hub_web allows an unauthenticated attacker to obtain an API token for another user's account via a crafted login link. Starting a CLI login with POST /api/auth/cli_session needs no authentication and returns a polling token, a confirmation URL that carries that token, and a confirmation code. When a signed-in user opens that URL and clicks Confirm, NervesHub.Accounts.verify_cli_session_token/2 creates an API token for the user's account and stores it on the login, and the next poll of GET /api/auth/cli_session/<token> returns it to whoever started the login. The confirmation code is only displayed and is never compared with anything, so it cannot show that the person confirming is the one who started the login. The confirm form carries a CSRF token and the page cannot be framed, so the attacker has to persuade the user to open the link and click the button themselves. This issue affects nerves_hub: from 2.5.0 before 2.5.3.
    CWE
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 2.5.0 , < 2.5.3 (other)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 2.5.0 , < 2.5.3 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: e6bf11a812d7aa8b69d854ba8ef02d65516f5564 , < 3131f1701194b581ab4b3e5e9d81194fc9c83508 (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.API.UserController\u0027",
                "\u0027Elixir.NervesHubWeb.SessionController\u0027",
                "\u0027Elixir.NervesHub.Accounts\u0027",
                "\u0027Elixir.NervesHubWeb.Router\u0027"
              ],
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io%2Fnerves-hub",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/controllers/api/user_controller.ex",
                "lib/nerves_hub_web/controllers/session_controller.ex",
                "lib/nerves_hub/accounts.ex",
                "lib/nerves_hub_web/router.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:cli_session/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:check_cli_session/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:cli/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:cli_confirm/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:generate_cli_session_token/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:cli_session_waiting?/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:verify_cli_session_token/2"
                }
              ],
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "2.5.0",
                  "versionType": "other"
                }
              ]
            },
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.API.UserController\u0027",
                "\u0027Elixir.NervesHubWeb.SessionController\u0027",
                "\u0027Elixir.NervesHub.Accounts\u0027",
                "\u0027Elixir.NervesHubWeb.Router\u0027"
              ],
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/controllers/api/user_controller.ex",
                "lib/nerves_hub_web/controllers/session_controller.ex",
                "lib/nerves_hub/accounts.ex",
                "lib/nerves_hub_web/router.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:cli_session/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:check_cli_session/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:cli/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:cli_confirm/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:generate_cli_session_token/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:cli_session_waiting?/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:verify_cli_session_token/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "2.5.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.API.UserController\u0027",
                "\u0027Elixir.NervesHubWeb.SessionController\u0027",
                "\u0027Elixir.NervesHub.Accounts\u0027",
                "\u0027Elixir.NervesHubWeb.Router\u0027"
              ],
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/controllers/api/user_controller.ex",
                "lib/nerves_hub_web/controllers/session_controller.ex",
                "lib/nerves_hub/accounts.ex",
                "lib/nerves_hub_web/router.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:cli_session/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:check_cli_session/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:cli/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:cli_confirm/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:generate_cli_session_token/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:cli_session_waiting?/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:verify_cli_session_token/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "3131f1701194b581ab4b3e5e9d81194fc9c83508",
                  "status": "affected",
                  "version": "e6bf11a812d7aa8b69d854ba8ef02d65516f5564",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.5.3",
                      "versionStartIncluding": "2.5.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "Amr Kadry"
            },
            {
              "lang": "en",
              "type": "reporter",
              "value": "Amr Kadry"
            },
            {
              "lang": "en",
              "type": "remediation developer",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "coordinator",
              "value": "Jonatan M\u00e4nnchen / EEF"
            }
          ],
          "dateAssigned": "2026-11-09T22:12:23.000Z",
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eSession Fixation vulnerability in the CLI login flow in nerves-hub nerves_hub_web allows an unauthenticated attacker to obtain an API token for another user\u0027s account via a crafted login link. Starting a CLI login with \u003ccode\u003ePOST /api/auth/cli_session\u003c/code\u003e needs no authentication and returns a polling token, a confirmation URL that carries that token, and a confirmation code. When a signed-in user opens that URL and clicks Confirm, \u003ccode\u003eNervesHub.Accounts.verify_cli_session_token/2\u003c/code\u003e creates an API token for the user\u0027s account and stores it on the login, and the next poll of \u003ccode\u003eGET /api/auth/cli_session/\u0026lt;token\u0026gt;\u003c/code\u003e returns it to whoever started the login.\u003c/p\u003e\n\u003cp\u003eThe confirmation code is only displayed and is never compared with anything, so it cannot show that the person confirming is the one who started the login. The confirm form carries a CSRF token and the page cannot be framed, so the attacker has to persuade the user to open the link and click the button themselves.\u003c/p\u003e\n\u003cp\u003eThis issue affects nerves_hub: from 2.5.0 before 2.5.3.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Session Fixation vulnerability in the CLI login flow in nerves-hub nerves_hub_web allows an unauthenticated attacker to obtain an API token for another user\u0027s account via a crafted login link. Starting a CLI login with `POST /api/auth/cli_session` needs no authentication and returns a polling token, a confirmation URL that carries that token, and a confirmation code. When a signed-in user opens that URL and clicks Confirm, `NervesHub.Accounts.verify_cli_session_token/2` creates an API token for the user\u0027s account and stores it on the login, and the next poll of `GET /api/auth/cli_session/\u003ctoken\u003e` returns it to whoever started the login.\n\nThe confirmation code is only displayed and is never compared with anything, so it cannot show that the person confirming is the one who started the login. The confirm form carries a CSRF token and the page cannot be framed, so the attacker has to persuade the user to open the link and click the button themselves.\n\nThis issue affects nerves_hub: from 2.5.0 before 2.5.3."
                }
              ],
              "value": "Session Fixation vulnerability in the CLI login flow in nerves-hub nerves_hub_web allows an unauthenticated attacker to obtain an API token for another user\u0027s account via a crafted login link. Starting a CLI login with POST /api/auth/cli_session needs no authentication and returns a polling token, a confirmation URL that carries that token, and a confirmation code. When a signed-in user opens that URL and clicks Confirm, NervesHub.Accounts.verify_cli_session_token/2 creates an API token for the user\u0027s account and stores it on the login, and the next poll of GET /api/auth/cli_session/\u003ctoken\u003e returns it to whoever started the login.\n\nThe confirmation code is only displayed and is never compared with anything, so it cannot show that the person confirming is the one who started the login. The confirm form carries a CSRF token and the page cannot be framed, so the attacker has to persuade the user to open the link and click the button themselves.\n\nThis issue affects nerves_hub: from 2.5.0 before 2.5.3."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-61",
              "descriptions": [
                {
                  "lang": "en",
                  "supportingMedia": [
                    {
                      "base64": false,
                      "type": "text/html",
                      "value": "\u003cp\u003eAn attacker with no account obtains an API token for any NervesHub user they can persuade to open a link and click Confirm. The token acts as that user through the API in every organization they belong to, at their role, until the user deletes it on the account page. Signing out of the web does not revoke it.\u003c/p\u003e"
                    },
                    {
                      "base64": false,
                      "type": "text/markdown",
                      "value": "An attacker with no account obtains an API token for any NervesHub user they can persuade to open a link and click Confirm. The token acts as that user through the API in every organization they belong to, at their role, until the user deletes it on the account page. Signing out of the web does not revoke it."
                    }
                  ],
                  "value": "An attacker with no account obtains an API token for any NervesHub user they can persuade to open a link and click Confirm. The token acts as that user through the API in every organization they belong to, at their role, until the user deletes it on the account page. Signing out of the web does not revoke it."
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "NONE",
                "attackVector": "NETWORK",
                "baseScore": 9.4,
                "baseSeverity": "CRITICAL",
                "privilegesRequired": "NONE",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "HIGH",
                "subConfidentialityImpact": "HIGH",
                "subIntegrityImpact": "HIGH",
                "userInteraction": "ACTIVE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
                "version": "4.0",
                "vulnAvailabilityImpact": "HIGH",
                "vulnConfidentialityImpact": "HIGH",
                "vulnIntegrityImpact": "HIGH",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-384",
                  "description": "CWE-384 Session Fixation",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-11-10T10:44:15.336Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "name": "CLI login links give an API token to whoever sent them",
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-9vr9-463q-46qp"
            },
            {
              "name": "EEF CNA record for CVE-2026-119446",
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-119446.html"
            },
            {
              "name": "OSV record EEF-CVE-2026-119446",
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-119446"
            },
            {
              "name": "Introducing commit e6bf11a in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/e6bf11a812d7aa8b69d854ba8ef02d65516f5564"
            },
            {
              "name": "Fix commit 3131f17 in nerves-hub/nerves_hub_web",
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/3131f1701194b581ab4b3e5e9d81194fc9c83508"
            }
          ],
          "solutions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eUpgrading does not revoke tokens already handed out. A token from a CLI login is an ordinary API token with a note the client chose, so one an attacker obtained looks like any other. After upgrading, users should review their API tokens on the account page and delete any they do not recognise.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Upgrading does not revoke tokens already handed out. A token from a CLI login is an ordinary API token with a note the client chose, so one an attacker obtained looks like any other. After upgrading, users should review their API tokens on the account page and delete any they do not recognise."
                }
              ],
              "value": "Upgrading does not revoke tokens already handed out. A token from a CLI login is an ordinary API token with a note the client chose, so one an attacker obtained looks like any other. After upgrading, users should review their API tokens on the account page and delete any they do not recognise."
            }
          ],
          "source": {
            "discovery": "UNKNOWN"
          },
          "title": "Session fixation in the CLI login flow allows an unauthenticated attacker to obtain a user\u0027s API token",
          "workarounds": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eBlock \u003ccode\u003ePOST /api/auth/cli_session\u003c/code\u003e at the reverse proxy until the upgrade. It is the only entry point that creates a CLI login, so no link can be produced while it is blocked. CLIs can still sign in with an email and password (\u003ccode\u003enh user auth\u003c/code\u003e, \u003ccode\u003ePOST /api/users/auth\u003c/code\u003e), or use an API token created on the account page.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Block `POST /api/auth/cli_session` at the reverse proxy until the upgrade. It is the only entry point that creates a CLI login, so no link can be produced while it is blocked. CLIs can still sign in with an email and password (`nh user auth`, `POST /api/users/auth`), or use an API token created on the account page."
                }
              ],
              "value": "Block POST /api/auth/cli_session at the reverse proxy until the upgrade. It is the only entry point that creates a CLI login, so no link can be produced while it is blocked. CLIs can still sign in with an email and password (nh user auth, POST /api/users/auth), or use an API token created on the account page."
            }
          ],
          "x_proofOfConcept": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003col\u003e\n\u003cli\u003eWith no account, run \u003ccode\u003ecurl -X POST https://\u0026lt;host\u0026gt;/api/auth/cli_session\u003c/code\u003e. Note \u003ccode\u003etoken\u003c/code\u003e, \u003ccode\u003eurl\u003c/code\u003e and \u003ccode\u003econfirmation_code\u003c/code\u003e from the response.\u003c/li\u003e\n\u003cli\u003eSend \u003ccode\u003eurl\u003c/code\u003e to someone with a NervesHub account, with a message such as \"Please verify your NervesHub CLI access. Your code is \u003ccode\u003e\u0026lt;confirmation_code\u0026gt;\u003c/code\u003e.\"\u003c/li\u003e\n\u003cli\u003eThey open it while signed in, see the code they were told to expect, and click \"Confirm login\".\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ecurl https://\u0026lt;host\u0026gt;/api/auth/cli_session/\u0026lt;token\u0026gt;\u003c/code\u003e returns \u003ccode\u003e\"status\": \"ready\"\u003c/code\u003e and a \u003ccode\u003euser_token\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eSend API requests with \u003ccode\u003eAuthorization: token \u0026lt;user_token\u0026gt;\u003c/code\u003e. They run as the person who confirmed.\u003c/li\u003e\n\u003c/ol\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "1. With no account, run `curl -X POST https://\u003chost\u003e/api/auth/cli_session`. Note `token`, `url` and `confirmation_code` from the response.\n2. Send `url` to someone with a NervesHub account, with a message such as \"Please verify your NervesHub CLI access. Your code is `\u003cconfirmation_code\u003e`.\"\n3. They open it while signed in, see the code they were told to expect, and click \"Confirm login\".\n4. `curl https://\u003chost\u003e/api/auth/cli_session/\u003ctoken\u003e` returns `\"status\": \"ready\"` and a `user_token`.\n5. Send API requests with `Authorization: token \u003cuser_token\u003e`. They run as the person who confirmed."
                }
              ],
              "value": "* With no account, run curl -X POST https://\u003chost\u003e/api/auth/cli_session. Note token, url and confirmation_code from the response.\n* Send url to someone with a NervesHub account, with a message such as \"Please verify your NervesHub CLI access. Your code is \u003cconfirmation_code\u003e.\"\n* They open it while signed in, see the code they were told to expect, and click \"Confirm login\".\n* curl https://\u003chost\u003e/api/auth/cli_session/\u003ctoken\u003e returns \"status\": \"ready\" and a user_token.\n* Send API requests with Authorization: token \u003cuser_token\u003e. They run as the person who confirmed."
            }
          ],
          "x_technicalAnalysis": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003e\u003cstrong\u003e1. Starting a login.\u003c/strong\u003e \u003ccode\u003enh user login\u003c/code\u003e, like any client of this flow, calls \u003ccode\u003ePOST /api/auth/cli_session\u003c/code\u003e, which needs no authentication. \u003ccode\u003eNervesHubWeb.API.UserController.cli_session/2\u003c/code\u003e calls \u003ccode\u003eNervesHub.Accounts.generate_cli_session_token/1\u003c/code\u003e, which stores a waiting login in the CLI session cache under a random UUID token with a six-digit confirmation code, and responds with the token, the code, and the URL \u003ccode\u003e/auth/cli/\u0026lt;token\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Approving a login.\u003c/strong\u003e \u003ccode\u003eGET /auth/cli/\u0026lt;token\u0026gt;\u003c/code\u003e requires a signed-in web user. \u003ccode\u003eNervesHubWeb.SessionController.cli/2\u003c/code\u003e finds the login by the token from the URL, shows the code, and renders a form that posts back to \u003ccode\u003e/auth/cli/\u0026lt;token\u0026gt;\u003c/code\u003e. The form carries no code field. \u003ccode\u003ecli_confirm/2\u003c/code\u003e passes the signed-in user and the token to \u003ccode\u003eNervesHub.Accounts.verify_cli_session_token/2\u003c/code\u003e, which creates an API token for that user with \u003ccode\u003ecreate_user_api_token/2\u003c/code\u003e and stores it on the login. Nothing compares the confirmation code with anything.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. Collecting the token.\u003c/strong\u003e \u003ccode\u003eGET /api/auth/cli_session/\u0026lt;token\u0026gt;\u003c/code\u003e, also unauthenticated, returns the stored API token once the login is confirmed. Whoever started the login holds the token from step 1 and can poll until it arrives. The token is an ordinary API token and stays valid until the user deletes it.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "**1. Starting a login.** `nh user login`, like any client of this flow, calls `POST /api/auth/cli_session`, which needs no authentication. `NervesHubWeb.API.UserController.cli_session/2` calls `NervesHub.Accounts.generate_cli_session_token/1`, which stores a waiting login in the CLI session cache under a random UUID token with a six-digit confirmation code, and responds with the token, the code, and the URL `/auth/cli/\u003ctoken\u003e`.\n\n**2. Approving a login.** `GET /auth/cli/\u003ctoken\u003e` requires a signed-in web user. `NervesHubWeb.SessionController.cli/2` finds the login by the token from the URL, shows the code, and renders a form that posts back to `/auth/cli/\u003ctoken\u003e`. The form carries no code field. `cli_confirm/2` passes the signed-in user and the token to `NervesHub.Accounts.verify_cli_session_token/2`, which creates an API token for that user with `create_user_api_token/2` and stores it on the login. Nothing compares the confirmation code with anything.\n\n**3. Collecting the token.** `GET /api/auth/cli_session/\u003ctoken\u003e`, also unauthenticated, returns the stored API token once the login is confirmed. Whoever started the login holds the token from step 1 and can poll until it arrives. The token is an ordinary API token and stays valid until the user deletes it."
                }
              ],
              "value": "1. Starting a login. nh user login, like any client of this flow, calls POST /api/auth/cli_session, which needs no authentication. NervesHubWeb.API.UserController.cli_session/2 calls NervesHub.Accounts.generate_cli_session_token/1, which stores a waiting login in the CLI session cache under a random UUID token with a six-digit confirmation code, and responds with the token, the code, and the URL /auth/cli/\u003ctoken\u003e.\n\n2. Approving a login. GET /auth/cli/\u003ctoken\u003e requires a signed-in web user. NervesHubWeb.SessionController.cli/2 finds the login by the token from the URL, shows the code, and renders a form that posts back to /auth/cli/\u003ctoken\u003e. The form carries no code field. cli_confirm/2 passes the signed-in user and the token to NervesHub.Accounts.verify_cli_session_token/2, which creates an API token for that user with create_user_api_token/2 and stores it on the login. Nothing compares the confirmation code with anything.\n\n3. Collecting the token. GET /api/auth/cli_session/\u003ctoken\u003e, also unauthenticated, returns the stored API token once the login is confirmed. Whoever started the login holds the token from step 1 and can poll until it arrives. The token is an ordinary API token and stays valid until the user deletes it."
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-119446",
        "datePublished": "2026-11-10T10:44:15.336Z",
        "dateReserved": "2026-11-07T21:44:37.269Z",
        "dateUpdated": "2026-11-10T10:44:15.336Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2026-120286 (GCVE-0-2026-120286)

    Vulnerability from cvelistv5 – Published: 2026-11-10 10:44 – Updated: 2026-11-10 10:44
    VLAI
    Title
    Missing authorization in device and deployment group page events allows view-role members to change devices and deployment groups
    Summary
    Missing Authorization vulnerability in the device and deployment group pages in nerves-hub nerves_hub_web allows an authenticated organization member with the view role to change devices and deployment groups via LiveView events reserved for the manage role. The event handlers that add and delete device certificates, add and remove tags, overwrite or clear a device's manual location, remove a device from its deployment group, move matched devices into a deployment group or remove unmatched ones, import devices from a CSV, and start, retry or delete firmware delta builds carry out the change without calling authorized!/2, and the pages render the controls to every member. A view member can add a certificate that lets whoever holds its key connect as the device, delete a certificate so the device can no longer authenticate with it, and change which deployment group, and so which firmware release, a device receives. Everything stays within the organizations the member belongs to. This issue affects nerves_hub: from 1.0.0 before 2.5.3.
    CWE
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (other)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 7b511764a468bb2c1f33945874faa86af2255c0c , < 34ea112472a460319c1e1dec8ad0ffc37df05ffe (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.DetailsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Live.DeploymentGroups.Show\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027"
              ],
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io%2Fnerves-hub",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/components/device_page/settings_tab.ex",
                "lib/nerves_hub_web/components/device_page/details_tab.ex",
                "lib/nerves_hub_web/live/deployment_groups/show.ex",
                "lib/nerves_hub_web/components/deployment_group_page/summary.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:handle_progress/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.DetailsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.DeploymentGroups.Show\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027:handle_progress/3"
                }
              ],
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "other"
                }
              ]
            },
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.DetailsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Live.DeploymentGroups.Show\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027"
              ],
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/components/device_page/settings_tab.ex",
                "lib/nerves_hub_web/components/device_page/details_tab.ex",
                "lib/nerves_hub_web/live/deployment_groups/show.ex",
                "lib/nerves_hub_web/components/deployment_group_page/summary.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:handle_progress/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.DetailsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.DeploymentGroups.Show\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027:handle_progress/3"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.DetailsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Live.DeploymentGroups.Show\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027"
              ],
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/components/device_page/settings_tab.ex",
                "lib/nerves_hub_web/components/device_page/details_tab.ex",
                "lib/nerves_hub_web/live/deployment_groups/show.ex",
                "lib/nerves_hub_web/components/deployment_group_page/summary.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:handle_progress/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.DetailsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.DeploymentGroups.Show\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DeploymentGroupPage.Summary\u0027:handle_progress/3"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "34ea112472a460319c1e1dec8ad0ffc37df05ffe",
                  "status": "affected",
                  "version": "7b511764a468bb2c1f33945874faa86af2255c0c",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.5.3",
                      "versionStartIncluding": "1.0.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "arbor-s"
            },
            {
              "lang": "en",
              "type": "reporter",
              "value": "arbor-s"
            },
            {
              "lang": "en",
              "type": "remediation developer",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "coordinator",
              "value": "Jonatan M\u00e4nnchen / EEF"
            }
          ],
          "dateAssigned": "2026-11-09T22:27:53.000Z",
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eMissing Authorization vulnerability in the device and deployment group pages in nerves-hub nerves_hub_web allows an authenticated organization member with the \u003ccode\u003eview\u003c/code\u003e role to change devices and deployment groups via LiveView events reserved for the \u003ccode\u003emanage\u003c/code\u003e role. The event handlers that add and delete device certificates, add and remove tags, overwrite or clear a device\u0027s manual location, remove a device from its deployment group, move matched devices into a deployment group or remove unmatched ones, import devices from a CSV, and start, retry or delete firmware delta builds carry out the change without calling \u003ccode\u003eauthorized!/2\u003c/code\u003e, and the pages render the controls to every member.\u003c/p\u003e\n\u003cp\u003eA \u003ccode\u003eview\u003c/code\u003e member can add a certificate that lets whoever holds its key connect as the device, delete a certificate so the device can no longer authenticate with it, and change which deployment group, and so which firmware release, a device receives. Everything stays within the organizations the member belongs to.\u003c/p\u003e\n\u003cp\u003eThis issue affects nerves_hub: from 1.0.0 before 2.5.3.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Missing Authorization vulnerability in the device and deployment group pages in nerves-hub nerves_hub_web allows an authenticated organization member with the `view` role to change devices and deployment groups via LiveView events reserved for the `manage` role. The event handlers that add and delete device certificates, add and remove tags, overwrite or clear a device\u0027s manual location, remove a device from its deployment group, move matched devices into a deployment group or remove unmatched ones, import devices from a CSV, and start, retry or delete firmware delta builds carry out the change without calling `authorized!/2`, and the pages render the controls to every member.\n\nA `view` member can add a certificate that lets whoever holds its key connect as the device, delete a certificate so the device can no longer authenticate with it, and change which deployment group, and so which firmware release, a device receives. Everything stays within the organizations the member belongs to.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3."
                }
              ],
              "value": "Missing Authorization vulnerability in the device and deployment group pages in nerves-hub nerves_hub_web allows an authenticated organization member with the view role to change devices and deployment groups via LiveView events reserved for the manage role. The event handlers that add and delete device certificates, add and remove tags, overwrite or clear a device\u0027s manual location, remove a device from its deployment group, move matched devices into a deployment group or remove unmatched ones, import devices from a CSV, and start, retry or delete firmware delta builds carry out the change without calling authorized!/2, and the pages render the controls to every member.\n\nA view member can add a certificate that lets whoever holds its key connect as the device, delete a certificate so the device can no longer authenticate with it, and change which deployment group, and so which firmware release, a device receives. Everything stays within the organizations the member belongs to.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-1",
              "descriptions": [
                {
                  "lang": "en",
                  "supportingMedia": [
                    {
                      "base64": false,
                      "type": "text/html",
                      "value": "\u003cp\u003eAn organization member with the view role can add or delete a device\u0027s certificates, change its tags and location, and move devices into or out of deployment groups, which changes the firmware release they receive. An added certificate lets whoever holds its key connect as the device and persists after the member leaves. Everything stays within the organizations the member belongs to.\u003c/p\u003e"
                    },
                    {
                      "base64": false,
                      "type": "text/markdown",
                      "value": "An organization member with the view role can add or delete a device\u0027s certificates, change its tags and location, and move devices into or out of deployment groups, which changes the firmware release they receive. An added certificate lets whoever holds its key connect as the device and persists after the member leaves. Everything stays within the organizations the member belongs to."
                    }
                  ],
                  "value": "An organization member with the view role can add or delete a device\u0027s certificates, change its tags and location, and move devices into or out of deployment groups, which changes the firmware release they receive. An added certificate lets whoever holds its key connect as the device and persists after the member leaves. Everything stays within the organizations the member belongs to."
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "NONE",
                "attackVector": "NETWORK",
                "baseScore": 5.3,
                "baseSeverity": "MEDIUM",
                "privilegesRequired": "LOW",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "NONE",
                "subConfidentialityImpact": "NONE",
                "subIntegrityImpact": "LOW",
                "userInteraction": "NONE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:L/SA:N",
                "version": "4.0",
                "vulnAvailabilityImpact": "LOW",
                "vulnConfidentialityImpact": "LOW",
                "vulnIntegrityImpact": "LOW",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-862",
                  "description": "CWE-862 Missing Authorization",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-11-10T10:44:07.907Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "name": "Users with the \u2018view\u2019 role can change devices and deployment groups and read local shell output",
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-gxj7-m8x6-r644"
            },
            {
              "name": "EEF CNA record for CVE-2026-120286",
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-120286.html"
            },
            {
              "name": "OSV record EEF-CVE-2026-120286",
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-120286"
            },
            {
              "name": "Introducing commit 7b51176 in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/7b511764a468bb2c1f33945874faa86af2255c0c"
            },
            {
              "name": "Fix commit 34ea112 in nerves-hub/nerves_hub_web",
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/34ea112472a460319c1e1dec8ad0ffc37df05ffe"
            }
          ],
          "solutions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eUpgrading does not remove a certificate that a \u003ccode\u003eview\u003c/code\u003e member already added. After upgrading, review each device\u0027s certificates and delete any you do not recognize.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Upgrading does not remove a certificate that a `view` member already added. After upgrading, review each device\u0027s certificates and delete any you do not recognize."
                }
              ],
              "value": "Upgrading does not remove a certificate that a view member already added. After upgrading, review each device\u0027s certificates and delete any you do not recognize."
            }
          ],
          "source": {
            "discovery": "UNKNOWN"
          },
          "title": "Missing authorization in device and deployment group page events allows view-role members to change devices and deployment groups",
          "workarounds": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eNo configuration closes the hole. Until the upgrade, give the \u003ccode\u003eview\u003c/code\u003e role only to people you would trust with the \u003ccode\u003emanage\u003c/code\u003e role on that organization\u0027s devices.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "No configuration closes the hole. Until the upgrade, give the `view` role only to people you would trust with the `manage` role on that organization\u0027s devices."
                }
              ],
              "value": "No configuration closes the hole. Until the upgrade, give the view role only to people you would trust with the manage role on that organization\u0027s devices."
            }
          ],
          "x_proofOfConcept": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003col\u003e\n\u003cli\u003eCreate deployment group \u003ccode\u003ecanary\u003c/code\u003e with conditions matching tag \u003ccode\u003ebeta\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eCreate \u003ccode\u003edevice_outside_match\u003c/code\u003e in the same product with tag \u003ccode\u003ebeta\u003c/code\u003e and no deployment group, and \u003ccode\u003edevice_inside_mismatch\u003c/code\u003e in deployment group \u003ccode\u003ecanary\u003c/code\u003e with tag \u003ccode\u003efoo\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eSign in as a member of the organization with the \u003ccode\u003eview\u003c/code\u003e role and open \u003ccode\u003e/org/\u0026lt;org\u0026gt;/\u0026lt;product\u0026gt;/deployment_groups/canary\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eClick \u003ccode\u003eMove device(s)\u003c/code\u003e: \u003ccode\u003edevice_outside_match\u003c/code\u003e is now in \u003ccode\u003ecanary\u003c/code\u003e. Click \u003ccode\u003eRemove device(s)\u003c/code\u003e: \u003ccode\u003edevice_inside_mismatch\u003c/code\u003e no longer has a deployment group.\u003c/li\u003e\n\u003cli\u003eAs the same member, open a device in the product and add a tag. The tag is saved.\u003c/li\u003e\n\u003cli\u003eOn the device\u0027s settings tab, delete a certificate, or upload a self-signed one. The change is saved, and a client presenting the uploaded certificate connects as the device.\u003c/li\u003e\n\u003c/ol\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "1. Create deployment group `canary` with conditions matching tag `beta`.\n2. Create `device_outside_match` in the same product with tag `beta` and no deployment group, and `device_inside_mismatch` in deployment group `canary` with tag `foo`.\n3. Sign in as a member of the organization with the `view` role and open `/org/\u003corg\u003e/\u003cproduct\u003e/deployment_groups/canary`.\n4. Click `Move device(s)`: `device_outside_match` is now in `canary`. Click `Remove device(s)`: `device_inside_mismatch` no longer has a deployment group.\n5. As the same member, open a device in the product and add a tag. The tag is saved.\n6. On the device\u0027s settings tab, delete a certificate, or upload a self-signed one. The change is saved, and a client presenting the uploaded certificate connects as the device."
                }
              ],
              "value": "* Create deployment group canary with conditions matching tag beta.\n* Create device_outside_match in the same product with tag beta and no deployment group, and device_inside_mismatch in deployment group canary with tag foo.\n* Sign in as a member of the organization with the view role and open /org/\u003corg\u003e/\u003cproduct\u003e/deployment_groups/canary.\n* Click Move device(s): device_outside_match is now in canary. Click Remove device(s): device_inside_mismatch no longer has a deployment group.\n* As the same member, open a device in the product and add a tag. The tag is saved.\n* On the device\u0027s settings tab, delete a certificate, or upload a self-signed one. The change is saved, and a client presenting the uploaded certificate connects as the device."
            }
          ],
          "x_technicalAnalysis": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eThe dashboard reads the member\u0027s role into \u003ccode\u003ecurrent_scope\u003c/code\u003e when a page mounts, and each privileged event handler is expected to call \u003ccode\u003eauthorized!/2\u003c/code\u003e with the permission it needs, all of which resolve to the \u003ccode\u003emanage\u003c/code\u003e role in \u003ccode\u003elib/nerves_hub_web/helpers/authorization.ex\u003c/code\u003e. These handlers never do:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDevice settings tab\u003c/strong\u003e (\u003ccode\u003eNervesHubWeb.Components.DevicePage.SettingsTab\u003c/code\u003e): \u003ccode\u003ehooked_event/3\u003c/code\u003e for \u003ccode\u003edelete-certificate\u003c/code\u003e, and \u003ccode\u003ehandle_progress/3\u003c/code\u003e for the \u003ccode\u003ecertificate\u003c/code\u003e upload, which should require \u003ccode\u003edevice:update\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDevice details tab\u003c/strong\u003e (\u003ccode\u003eNervesHubWeb.Components.DevicePage.DetailsTab\u003c/code\u003e): \u003ccode\u003ehooked_event/3\u003c/code\u003e for \u003ccode\u003eadd-tag\u003c/code\u003e, \u003ccode\u003eremove-tag\u003c/code\u003e, \u003ccode\u003esave-location-changes\u003c/code\u003e and \u003ccode\u003eclear-manual-location-information\u003c/code\u003e (\u003ccode\u003edevice:update\u003c/code\u003e), and \u003ccode\u003eremove-from-deployment-group\u003c/code\u003e (\u003ccode\u003edevice:set-deployment-group\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeployment group page\u003c/strong\u003e (\u003ccode\u003eNervesHubWeb.Live.DeploymentGroups.Show\u003c/code\u003e and \u003ccode\u003eNervesHubWeb.Components.DeploymentGroupPage.Summary\u003c/code\u003e): \u003ccode\u003ehandle_event/3\u003c/code\u003e for \u003ccode\u003emove-matched-devices-to-deployment-group\u003c/code\u003e and \u003ccode\u003eremove-unmatched-devices-from-deployment-group\u003c/code\u003e, and \u003ccode\u003ehandle_progress/3\u003c/code\u003e for the \u003ccode\u003edevice_csv\u003c/code\u003e upload (\u003ccode\u003edevice:set-deployment-group\u003c/code\u003e), plus \u003ccode\u003egenerate-firmware-deltas\u003c/code\u003e, \u003ccode\u003eretry_delta\u003c/code\u003e and \u003ccode\u003edelete_delta\u003c/code\u003e (\u003ccode\u003edeployment_group:update\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eThe pages show every one of these controls to \u003ccode\u003eview\u003c/code\u003e members, so each change takes a click, and a client can send the events directly over the LiveView socket regardless.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "The dashboard reads the member\u0027s role into `current_scope` when a page mounts, and each privileged event handler is expected to call `authorized!/2` with the permission it needs, all of which resolve to the `manage` role in `lib/nerves_hub_web/helpers/authorization.ex`. These handlers never do:\n\n**Device settings tab** (`NervesHubWeb.Components.DevicePage.SettingsTab`): `hooked_event/3` for `delete-certificate`, and `handle_progress/3` for the `certificate` upload, which should require `device:update`.\n\n**Device details tab** (`NervesHubWeb.Components.DevicePage.DetailsTab`): `hooked_event/3` for `add-tag`, `remove-tag`, `save-location-changes` and `clear-manual-location-information` (`device:update`), and `remove-from-deployment-group` (`device:set-deployment-group`).\n\n**Deployment group page** (`NervesHubWeb.Live.DeploymentGroups.Show` and `NervesHubWeb.Components.DeploymentGroupPage.Summary`): `handle_event/3` for `move-matched-devices-to-deployment-group` and `remove-unmatched-devices-from-deployment-group`, and `handle_progress/3` for the `device_csv` upload (`device:set-deployment-group`), plus `generate-firmware-deltas`, `retry_delta` and `delete_delta` (`deployment_group:update`).\n\nThe pages show every one of these controls to `view` members, so each change takes a click, and a client can send the events directly over the LiveView socket regardless."
                }
              ],
              "value": "The dashboard reads the member\u0027s role into current_scope when a page mounts, and each privileged event handler is expected to call authorized!/2 with the permission it needs, all of which resolve to the manage role in lib/nerves_hub_web/helpers/authorization.ex. These handlers never do:\n\nDevice settings tab (NervesHubWeb.Components.DevicePage.SettingsTab): hooked_event/3 for delete-certificate, and handle_progress/3 for the certificate upload, which should require device:update.\n\nDevice details tab (NervesHubWeb.Components.DevicePage.DetailsTab): hooked_event/3 for add-tag, remove-tag, save-location-changes and clear-manual-location-information (device:update), and remove-from-deployment-group (device:set-deployment-group).\n\nDeployment group page (NervesHubWeb.Live.DeploymentGroups.Show and NervesHubWeb.Components.DeploymentGroupPage.Summary): handle_event/3 for move-matched-devices-to-deployment-group and remove-unmatched-devices-from-deployment-group, and handle_progress/3 for the device_csv upload (device:set-deployment-group), plus generate-firmware-deltas, retry_delta and delete_delta (deployment_group:update).\n\nThe pages show every one of these controls to view members, so each change takes a click, and a client can send the events directly over the LiveView socket regardless."
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-120286",
        "datePublished": "2026-11-10T10:44:07.907Z",
        "dateReserved": "2026-11-04T17:15:01.545Z",
        "dateUpdated": "2026-11-10T10:44:07.907Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2026-120997 (GCVE-0-2026-120997)

    Vulnerability from cvelistv5 – Published: 2026-11-10 10:43 – Updated: 2026-11-10 10:43
    VLAI
    Title
    Improper session invalidation keeps removed or demoted members' open pages and device consoles working at their old role
    Summary
    Improper Session Invalidation vulnerability in the dashboard pages, device console and local shell channels in nerves-hub nerves_hub_web allows an organization member who is removed or given a lower role to keep acting at their old role through anything they had open at the time. The dashboard reads the member's role once when a page mounts, in the assign_org_to_scope hook of NervesHubWeb.Auth, and the console, local shell and device event stream channels check membership once at join. Removing a member or changing their role tells none of them, so open pages keep carrying out actions and open consoles keep running commands on the device until the page reloads or the socket disconnects. The dashboard also opens consoles over NervesHubWeb.UserSocket with a token that holds only the user's id, is accepted for 24 hours, and is not revoked by signing out, which disconnects only the session's LiveViews. Whoever holds a token taken from a page can open consoles as that user, within the user's current membership, until it expires. This issue affects nerves_hub: from 1.0.0 before 2.5.3.
    CWE
    • CWE-1464 - Improper Session Invalidation
    • CWE-1465 - Improper Session Invalidation after User Logout
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (other)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: b5d2438677f79d10e91aeef20a7b5cd8cca0d63c , < a96497ddd4d24ab1847f7685e255bf96fd87b85b (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.UserSocket\u0027",
                "\u0027Elixir.NervesHubWeb.UserConsoleChannel\u0027",
                "\u0027Elixir.NervesHubWeb.UserLocalShellChannel\u0027",
                "\u0027Elixir.NervesHubWeb.DeviceEventsStreamChannel\u0027",
                "\u0027Elixir.NervesHubWeb.Auth\u0027",
                "\u0027Elixir.NervesHub.Accounts\u0027"
              ],
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io%2Fnerves-hub",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/channels/user_socket.ex",
                "lib/nerves_hub_web/channels/user_console_channel.ex",
                "lib/nerves_hub_web/channels/user_local_shell_channel.ex",
                "lib/nerves_hub_web/channels/device_events_stream_channel.ex",
                "lib/nerves_hub_web/auth.ex",
                "lib/nerves_hub/accounts.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserSocket\u0027:connect/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserSocket\u0027:id/1"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserConsoleChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserLocalShellChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.DeviceEventsStreamChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Auth\u0027:on_mount/4"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Auth\u0027:log_out_user/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:soft_delete_org_user/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:change_org_user_role/2"
                }
              ],
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "other"
                }
              ]
            },
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.UserSocket\u0027",
                "\u0027Elixir.NervesHubWeb.UserConsoleChannel\u0027",
                "\u0027Elixir.NervesHubWeb.UserLocalShellChannel\u0027",
                "\u0027Elixir.NervesHubWeb.DeviceEventsStreamChannel\u0027",
                "\u0027Elixir.NervesHubWeb.Auth\u0027",
                "\u0027Elixir.NervesHub.Accounts\u0027"
              ],
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/channels/user_socket.ex",
                "lib/nerves_hub_web/channels/user_console_channel.ex",
                "lib/nerves_hub_web/channels/user_local_shell_channel.ex",
                "lib/nerves_hub_web/channels/device_events_stream_channel.ex",
                "lib/nerves_hub_web/auth.ex",
                "lib/nerves_hub/accounts.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserSocket\u0027:connect/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserSocket\u0027:id/1"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserConsoleChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserLocalShellChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.DeviceEventsStreamChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Auth\u0027:on_mount/4"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Auth\u0027:log_out_user/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:soft_delete_org_user/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:change_org_user_role/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.UserSocket\u0027",
                "\u0027Elixir.NervesHubWeb.UserConsoleChannel\u0027",
                "\u0027Elixir.NervesHubWeb.UserLocalShellChannel\u0027",
                "\u0027Elixir.NervesHubWeb.DeviceEventsStreamChannel\u0027",
                "\u0027Elixir.NervesHubWeb.Auth\u0027",
                "\u0027Elixir.NervesHub.Accounts\u0027"
              ],
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/channels/user_socket.ex",
                "lib/nerves_hub_web/channels/user_console_channel.ex",
                "lib/nerves_hub_web/channels/user_local_shell_channel.ex",
                "lib/nerves_hub_web/channels/device_events_stream_channel.ex",
                "lib/nerves_hub_web/auth.ex",
                "lib/nerves_hub/accounts.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserSocket\u0027:connect/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserSocket\u0027:id/1"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserConsoleChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.UserLocalShellChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.DeviceEventsStreamChannel\u0027:join/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Auth\u0027:on_mount/4"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Auth\u0027:log_out_user/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:soft_delete_org_user/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:change_org_user_role/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "a96497ddd4d24ab1847f7685e255bf96fd87b85b",
                  "status": "affected",
                  "version": "b5d2438677f79d10e91aeef20a7b5cd8cca0d63c",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.5.3",
                      "versionStartIncluding": "1.0.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "reporter",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "remediation developer",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "coordinator",
              "value": "Jonatan M\u00e4nnchen / EEF"
            }
          ],
          "dateAssigned": "2026-11-09T23:03:50.000Z",
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eImproper Session Invalidation vulnerability in the dashboard pages, device console and local shell channels in nerves-hub nerves_hub_web allows an organization member who is removed or given a lower role to keep acting at their old role through anything they had open at the time. The dashboard reads the member\u0027s role once when a page mounts, in the \u003ccode\u003eassign_org_to_scope\u003c/code\u003e hook of \u003ccode\u003eNervesHubWeb.Auth\u003c/code\u003e, and the console, local shell and device event stream channels check membership once at join. Removing a member or changing their role tells none of them, so open pages keep carrying out actions and open consoles keep running commands on the device until the page reloads or the socket disconnects.\u003c/p\u003e\n\u003cp\u003eThe dashboard also opens consoles over \u003ccode\u003eNervesHubWeb.UserSocket\u003c/code\u003e with a token that holds only the user\u0027s id, is accepted for 24 hours, and is not revoked by signing out, which disconnects only the session\u0027s LiveViews. Whoever holds a token taken from a page can open consoles as that user, within the user\u0027s current membership, until it expires.\u003c/p\u003e\n\u003cp\u003eThis issue affects nerves_hub: from 1.0.0 before 2.5.3.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Improper Session Invalidation vulnerability in the dashboard pages, device console and local shell channels in nerves-hub nerves_hub_web allows an organization member who is removed or given a lower role to keep acting at their old role through anything they had open at the time. The dashboard reads the member\u0027s role once when a page mounts, in the `assign_org_to_scope` hook of `NervesHubWeb.Auth`, and the console, local shell and device event stream channels check membership once at join. Removing a member or changing their role tells none of them, so open pages keep carrying out actions and open consoles keep running commands on the device until the page reloads or the socket disconnects.\n\nThe dashboard also opens consoles over `NervesHubWeb.UserSocket` with a token that holds only the user\u0027s id, is accepted for 24 hours, and is not revoked by signing out, which disconnects only the session\u0027s LiveViews. Whoever holds a token taken from a page can open consoles as that user, within the user\u0027s current membership, until it expires.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3."
                }
              ],
              "value": "Improper Session Invalidation vulnerability in the dashboard pages, device console and local shell channels in nerves-hub nerves_hub_web allows an organization member who is removed or given a lower role to keep acting at their old role through anything they had open at the time. The dashboard reads the member\u0027s role once when a page mounts, in the assign_org_to_scope hook of NervesHubWeb.Auth, and the console, local shell and device event stream channels check membership once at join. Removing a member or changing their role tells none of them, so open pages keep carrying out actions and open consoles keep running commands on the device until the page reloads or the socket disconnects.\n\nThe dashboard also opens consoles over NervesHubWeb.UserSocket with a token that holds only the user\u0027s id, is accepted for 24 hours, and is not revoked by signing out, which disconnects only the session\u0027s LiveViews. Whoever holds a token taken from a page can open consoles as that user, within the user\u0027s current membership, until it expires.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-60",
              "descriptions": [
                {
                  "lang": "en",
                  "supportingMedia": [
                    {
                      "base64": false,
                      "type": "text/html",
                      "value": "\u003cp\u003eA member who is removed from an organization or given a lower role keeps their old access through whatever they had open at the time, including device consoles and local shells that run commands on the device, until the page reloads or the socket disconnects. Anyone holding the dashboard\u0027s socket token taken from a page can open consoles as that user, within that user\u0027s current permissions, for up to 24 hours after they sign out.\u003c/p\u003e"
                    },
                    {
                      "base64": false,
                      "type": "text/markdown",
                      "value": "A member who is removed from an organization or given a lower role keeps their old access through whatever they had open at the time, including device consoles and local shells that run commands on the device, until the page reloads or the socket disconnects. Anyone holding the dashboard\u0027s socket token taken from a page can open consoles as that user, within that user\u0027s current permissions, for up to 24 hours after they sign out."
                    }
                  ],
                  "value": "A member who is removed from an organization or given a lower role keeps their old access through whatever they had open at the time, including device consoles and local shells that run commands on the device, until the page reloads or the socket disconnects. Anyone holding the dashboard\u0027s socket token taken from a page can open consoles as that user, within that user\u0027s current permissions, for up to 24 hours after they sign out."
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "PRESENT",
                "attackVector": "NETWORK",
                "baseScore": 2.3,
                "baseSeverity": "LOW",
                "privilegesRequired": "LOW",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "NONE",
                "subConfidentialityImpact": "NONE",
                "subIntegrityImpact": "LOW",
                "userInteraction": "NONE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:L/SA:N",
                "version": "4.0",
                "vulnAvailabilityImpact": "NONE",
                "vulnConfidentialityImpact": "LOW",
                "vulnIntegrityImpact": "LOW",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-1464",
                  "description": "CWE-1464 Improper Session Invalidation",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            },
            {
              "descriptions": [
                {
                  "cweId": "CWE-1465",
                  "description": "CWE-1465 Improper Session Invalidation after User Logout",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-11-10T10:43:57.738Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "name": "Removing or demoting an org member doesn\u2019t close pages and consoles they already have open",
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-c7jf-qcx9-xxf8"
            },
            {
              "name": "EEF CNA record for CVE-2026-120997",
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-120997.html"
            },
            {
              "name": "OSV record EEF-CVE-2026-120997",
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-120997"
            },
            {
              "name": "Introducing commit b5d2438 in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/b5d2438677f79d10e91aeef20a7b5cd8cca0d63c"
            },
            {
              "name": "Fix commit a96497d in nerves-hub/nerves_hub_web",
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/a96497ddd4d24ab1847f7685e255bf96fd87b85b"
            }
          ],
          "source": {
            "discovery": "UNKNOWN"
          },
          "title": "Improper session invalidation keeps removed or demoted members\u0027 open pages and device consoles working at their old role",
          "workarounds": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eAfter removing a member or lowering their role, restart the web nodes. That closes every open page and console, and each one is checked again when it reconnects. A socket token cannot be revoked before it expires short of changing \u003ccode\u003eSECRET_KEY_BASE\u003c/code\u003e, which signs everyone out.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "After removing a member or lowering their role, restart the web nodes. That closes every open page and console, and each one is checked again when it reconnects. A socket token cannot be revoked before it expires short of changing `SECRET_KEY_BASE`, which signs everyone out."
                }
              ],
              "value": "After removing a member or lowering their role, restart the web nodes. That closes every open page and console, and each one is checked again when it reconnects. A socket token cannot be revoked before it expires short of changing SECRET_KEY_BASE, which signs everyone out."
            }
          ],
          "x_proofOfConcept": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003col\u003e\n\u003cli\u003eIn an organization, A is an admin and B has the manage role. B opens a device\u0027s page and its Console tab.\u003c/li\u003e\n\u003cli\u003eA removes B from the organization on the organization\u0027s Users page, or lowers B\u0027s role to view.\u003c/li\u003e\n\u003cli\u003eWithout reloading, B\u0027s open page still carries out actions and B\u0027s console still accepts input, until B reloads the page or the socket disconnects.\u003c/li\u003e\n\u003cli\u003eFor the socket token: while signed in, copy the \u003ccode\u003edata-user-token\u003c/code\u003e attribute of the \u003ccode\u003e#console\u003c/code\u003e element, sign out, and within 24 hours connect a Phoenix socket client to \u003ccode\u003e/socket/websocket\u003c/code\u003e with that token. Joining \u003ccode\u003euser:console:identifier-\u0026lt;device identifier\u0026gt;\u003c/code\u003e succeeds.\u003c/li\u003e\n\u003c/ol\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "1. In an organization, A is an admin and B has the manage role. B opens a device\u0027s page and its Console tab.\n2. A removes B from the organization on the organization\u0027s Users page, or lowers B\u0027s role to view.\n3. Without reloading, B\u0027s open page still carries out actions and B\u0027s console still accepts input, until B reloads the page or the socket disconnects.\n4. For the socket token: while signed in, copy the `data-user-token` attribute of the `#console` element, sign out, and within 24 hours connect a Phoenix socket client to `/socket/websocket` with that token. Joining `user:console:identifier-\u003cdevice identifier\u003e` succeeds."
                }
              ],
              "value": "* In an organization, A is an admin and B has the manage role. B opens a device\u0027s page and its Console tab.\n* A removes B from the organization on the organization\u0027s Users page, or lowers B\u0027s role to view.\n* Without reloading, B\u0027s open page still carries out actions and B\u0027s console still accepts input, until B reloads the page or the socket disconnects.\n* For the socket token: while signed in, copy the data-user-token attribute of the #console element, sign out, and within 24 hours connect a Phoenix socket client to /socket/websocket with that token. Joining user:console:identifier-\u003cdevice identifier\u003e succeeds."
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-120997",
        "datePublished": "2026-11-10T10:43:57.738Z",
        "dateReserved": "2026-11-05T14:30:01.700Z",
        "dateUpdated": "2026-11-10T10:43:57.738Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2026-120730 (GCVE-0-2026-120730)

    Vulnerability from cvelistv5 – Published: 2026-11-10 10:43 – Updated: 2026-11-10 10:43
    VLAI
    Title
    Mass assignment of organization, product and deployment group IDs allows users to write into other organizations and receive their firmware
    Summary
    Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in the device API, device forms, signing key and product forms and JITP settings in nerves-hub nerves_hub_web allows an authenticated user who manages devices in any organization to write into other organizations via submitted org_id, product_id and deployment_id fields. The device API create and update actions and the dashboard device forms pass the request parameters to NervesHub.Devices.Device.changeset/2, which casts the organization, product, deployment group and update state as given. The signing key and product forms merge the session's organization into the submitted parameters so that a submitted org_id wins, and the JITP changeset accepts any product_id. Each request requires the manage role only in the user's own organization, and any account can create one. A device placed on another organization's deployment group, or moved into its product, is sent that organization's firmware with a download URL. The user cannot change or control devices that already belong to the other organization, and cannot upload firmware to it. This issue affects nerves_hub: from 1.0.0 before 2.5.3.
    CWE
    • CWE-639 - Authorization Bypass Through User-Controlled Key
    • CWE-915 - Improperly Controlled Modification of Dynamically-Determined Object Attributes
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (other)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: f58d58233dbd355c58d3a140107ece18e8863f14 , < c367d7d17d07f843e78a70d15904665616dde964 (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027",
                "\u0027Elixir.NervesHub.Devices.Device\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Devices.New\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Org.SigningKeys\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Products.New\u0027",
                "\u0027Elixir.NervesHub.Devices.CACertificate.JITP\u0027",
                "\u0027Elixir.NervesHub.ManagedDeployments\u0027"
              ],
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io%2Fnerves-hub",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/controllers/api/device_controller.ex",
                "lib/nerves_hub/devices/device.ex",
                "lib/nerves_hub_web/components/device_page/settings_tab.ex",
                "lib/nerves_hub_web/live/devices/new.ex",
                "lib/nerves_hub_web/live/org/signing_keys.ex",
                "lib/nerves_hub_web/live/products/new.ex",
                "lib/nerves_hub/devices/ca_certificate/jitp.ex",
                "lib/nerves_hub/managed_deployments.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027:create/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027:update/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Devices.Device\u0027:changeset/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Devices.New\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Org.SigningKeys\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Products.New\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Devices.CACertificate.JITP\u0027:changeset/2"
                }
              ],
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "other"
                }
              ]
            },
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027",
                "\u0027Elixir.NervesHub.Devices.Device\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Devices.New\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Org.SigningKeys\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Products.New\u0027",
                "\u0027Elixir.NervesHub.Devices.CACertificate.JITP\u0027",
                "\u0027Elixir.NervesHub.ManagedDeployments\u0027"
              ],
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/controllers/api/device_controller.ex",
                "lib/nerves_hub/devices/device.ex",
                "lib/nerves_hub_web/components/device_page/settings_tab.ex",
                "lib/nerves_hub_web/live/devices/new.ex",
                "lib/nerves_hub_web/live/org/signing_keys.ex",
                "lib/nerves_hub_web/live/products/new.ex",
                "lib/nerves_hub/devices/ca_certificate/jitp.ex",
                "lib/nerves_hub/managed_deployments.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027:create/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027:update/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Devices.Device\u0027:changeset/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Devices.New\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Org.SigningKeys\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Products.New\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Devices.CACertificate.JITP\u0027:changeset/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027",
                "\u0027Elixir.NervesHub.Devices.Device\u0027",
                "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Devices.New\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Org.SigningKeys\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Products.New\u0027",
                "\u0027Elixir.NervesHub.Devices.CACertificate.JITP\u0027",
                "\u0027Elixir.NervesHub.ManagedDeployments\u0027"
              ],
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/controllers/api/device_controller.ex",
                "lib/nerves_hub/devices/device.ex",
                "lib/nerves_hub_web/components/device_page/settings_tab.ex",
                "lib/nerves_hub_web/live/devices/new.ex",
                "lib/nerves_hub_web/live/org/signing_keys.ex",
                "lib/nerves_hub_web/live/products/new.ex",
                "lib/nerves_hub/devices/ca_certificate/jitp.ex",
                "lib/nerves_hub/managed_deployments.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027:create/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.DeviceController\u0027:update/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Devices.Device\u0027:changeset/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Components.DevicePage.SettingsTab\u0027:hooked_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Devices.New\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Org.SigningKeys\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Products.New\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Devices.CACertificate.JITP\u0027:changeset/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "c367d7d17d07f843e78a70d15904665616dde964",
                  "status": "affected",
                  "version": "f58d58233dbd355c58d3a140107ece18e8863f14",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.5.3",
                      "versionStartIncluding": "1.0.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "reporter",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "remediation developer",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "coordinator",
              "value": "Jonatan M\u00e4nnchen / EEF"
            }
          ],
          "dateAssigned": "2026-11-09T22:45:21.000Z",
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eImproperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in the device API, device forms, signing key and product forms and JITP settings in nerves-hub nerves_hub_web allows an authenticated user who manages devices in any organization to write into other organizations via submitted \u003ccode\u003eorg_id\u003c/code\u003e, \u003ccode\u003eproduct_id\u003c/code\u003e and \u003ccode\u003edeployment_id\u003c/code\u003e fields. The device API create and update actions and the dashboard device forms pass the request parameters to \u003ccode\u003eNervesHub.Devices.Device.changeset/2\u003c/code\u003e, which casts the organization, product, deployment group and update state as given. The signing key and product forms merge the session\u0027s organization into the submitted parameters so that a submitted \u003ccode\u003eorg_id\u003c/code\u003e wins, and the JITP changeset accepts any \u003ccode\u003eproduct_id\u003c/code\u003e. Each request requires the \u003ccode\u003emanage\u003c/code\u003e role only in the user\u0027s own organization, and any account can create one.\u003c/p\u003e\n\u003cp\u003eA device placed on another organization\u0027s deployment group, or moved into its product, is sent that organization\u0027s firmware with a download URL. The user cannot change or control devices that already belong to the other organization, and cannot upload firmware to it.\u003c/p\u003e\n\u003cp\u003eThis issue affects nerves_hub: from 1.0.0 before 2.5.3.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in the device API, device forms, signing key and product forms and JITP settings in nerves-hub nerves_hub_web allows an authenticated user who manages devices in any organization to write into other organizations via submitted `org_id`, `product_id` and `deployment_id` fields. The device API create and update actions and the dashboard device forms pass the request parameters to `NervesHub.Devices.Device.changeset/2`, which casts the organization, product, deployment group and update state as given. The signing key and product forms merge the session\u0027s organization into the submitted parameters so that a submitted `org_id` wins, and the JITP changeset accepts any `product_id`. Each request requires the `manage` role only in the user\u0027s own organization, and any account can create one.\n\nA device placed on another organization\u0027s deployment group, or moved into its product, is sent that organization\u0027s firmware with a download URL. The user cannot change or control devices that already belong to the other organization, and cannot upload firmware to it.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3."
                }
              ],
              "value": "Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in the device API, device forms, signing key and product forms and JITP settings in nerves-hub nerves_hub_web allows an authenticated user who manages devices in any organization to write into other organizations via submitted org_id, product_id and deployment_id fields. The device API create and update actions and the dashboard device forms pass the request parameters to NervesHub.Devices.Device.changeset/2, which casts the organization, product, deployment group and update state as given. The signing key and product forms merge the session\u0027s organization into the submitted parameters so that a submitted org_id wins, and the JITP changeset accepts any product_id. Each request requires the manage role only in the user\u0027s own organization, and any account can create one.\n\nA device placed on another organization\u0027s deployment group, or moved into its product, is sent that organization\u0027s firmware with a download URL. The user cannot change or control devices that already belong to the other organization, and cannot upload firmware to it.\n\nThis issue affects nerves_hub: from 1.0.0 before 2.5.3."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-77",
              "descriptions": [
                {
                  "lang": "en",
                  "supportingMedia": [
                    {
                      "base64": false,
                      "type": "text/html",
                      "value": "\u003cp\u003eAny user who manages devices in an organization of their own can attach their device to another organization\u0027s deployment group and receive its firmware, move devices into that organization\u0027s product, add firmware signing keys and products to it, and have a certificate authority provision devices into its product. Devices that already belong to the other organization are not controlled, and no firmware can be uploaded to it.\u003c/p\u003e"
                    },
                    {
                      "base64": false,
                      "type": "text/markdown",
                      "value": "Any user who manages devices in an organization of their own can attach their device to another organization\u0027s deployment group and receive its firmware, move devices into that organization\u0027s product, add firmware signing keys and products to it, and have a certificate authority provision devices into its product. Devices that already belong to the other organization are not controlled, and no firmware can be uploaded to it."
                    }
                  ],
                  "value": "Any user who manages devices in an organization of their own can attach their device to another organization\u0027s deployment group and receive its firmware, move devices into that organization\u0027s product, add firmware signing keys and products to it, and have a certificate authority provision devices into its product. Devices that already belong to the other organization are not controlled, and no firmware can be uploaded to it."
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "NONE",
                "attackVector": "NETWORK",
                "baseScore": 8.6,
                "baseSeverity": "HIGH",
                "privilegesRequired": "LOW",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "NONE",
                "subConfidentialityImpact": "NONE",
                "subIntegrityImpact": "NONE",
                "userInteraction": "NONE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
                "version": "4.0",
                "vulnAvailabilityImpact": "NONE",
                "vulnConfidentialityImpact": "HIGH",
                "vulnIntegrityImpact": "HIGH",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-639",
                  "description": "CWE-639 Authorization Bypass Through User-Controlled Key",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            },
            {
              "descriptions": [
                {
                  "cweId": "CWE-915",
                  "description": "CWE-915 Improperly Controlled Modification of Dynamically-Determined Object Attributes",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-11-10T10:43:47.924Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "name": "Users can receive another organization\u2019s firmware and add devices, signing keys and products to it",
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-rpp7-x9hm-jrmr"
            },
            {
              "name": "EEF CNA record for CVE-2026-120730",
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-120730.html"
            },
            {
              "name": "OSV record EEF-CVE-2026-120730",
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-120730"
            },
            {
              "name": "Introducing commit f58d582 in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/f58d58233dbd355c58d3a140107ece18e8863f14"
            },
            {
              "name": "Fix commit c367d7d in nerves-hub/nerves_hub_web",
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/c367d7d17d07f843e78a70d15904665616dde964"
            }
          ],
          "solutions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eThe fixed release\u0027s migration takes every device off a deployment group that belongs to a different product and logs each one. Upgrading undoes nothing else done this way. After upgrading, check the database for devices whose organization does not own their product, for signing keys in \u003ccode\u003eorg_keys\u003c/code\u003e whose \u003ccode\u003ecreated_by_id\u003c/code\u003e has never been a member of the key\u0027s organization, and for certificate authorities whose JITP product belongs to another organization, and remove what you do not recognize. A device moved wholly into another organization, and a product added to one, leave nothing that sets them apart, so review your organization\u0027s devices and products as well.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "The fixed release\u0027s migration takes every device off a deployment group that belongs to a different product and logs each one. Upgrading undoes nothing else done this way. After upgrading, check the database for devices whose organization does not own their product, for signing keys in `org_keys` whose `created_by_id` has never been a member of the key\u0027s organization, and for certificate authorities whose JITP product belongs to another organization, and remove what you do not recognize. A device moved wholly into another organization, and a product added to one, leave nothing that sets them apart, so review your organization\u0027s devices and products as well."
                }
              ],
              "value": "The fixed release\u0027s migration takes every device off a deployment group that belongs to a different product and logs each one. Upgrading undoes nothing else done this way. After upgrading, check the database for devices whose organization does not own their product, for signing keys in org_keys whose created_by_id has never been a member of the key\u0027s organization, and for certificate authorities whose JITP product belongs to another organization, and remove what you do not recognize. A device moved wholly into another organization, and a product added to one, leave nothing that sets them apart, so review your organization\u0027s devices and products as well."
            }
          ],
          "source": {
            "discovery": "UNKNOWN"
          },
          "title": "Mass assignment of organization, product and deployment group IDs allows users to write into other organizations and receive their firmware",
          "x_proofOfConcept": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eAs an admin of organization A, with organization B having an active deployment group whose current release is firmware B:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003ePOST /api/orgs/\u0026lt;A\u0026gt;/products/\u0026lt;A\u0027s product\u0026gt;/devices\u003c/code\u003e with \u003ccode\u003e{\"identifier\": \"planted\", \"deployment_id\": \u0026lt;B\u0027s deployment group ID\u0026gt;}\u003c/code\u003e. The device is created in A, on B\u0027s deployment group.\u003c/li\u003e\n\u003cli\u003eCreate a device key for it, then connect it, reporting the platform and architecture of firmware B. When the deployment runs, the device is sent an update for firmware B with its download URL.\u003c/li\u003e\n\u003cli\u003eTo move a device instead, \u003ccode\u003ePUT /api/orgs/\u0026lt;A\u0026gt;/products/\u0026lt;A\u0027s product\u0026gt;/devices/\u0026lt;identifier\u0026gt;\u003c/code\u003e with \u003ccode\u003e{\"org_id\": \u0026lt;B\u0027s ID\u0026gt;, \"product_id\": \u0026lt;B\u0027s product ID\u0026gt;}\u003c/code\u003e. The response is \u003ccode\u003e201\u003c/code\u003e, and the device is now in B\u0027s product.\u003c/li\u003e\n\u003c/ol\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "As an admin of organization A, with organization B having an active deployment group whose current release is firmware B:\n\n1. `POST /api/orgs/\u003cA\u003e/products/\u003cA\u0027s product\u003e/devices` with `{\"identifier\": \"planted\", \"deployment_id\": \u003cB\u0027s deployment group ID\u003e}`. The device is created in A, on B\u0027s deployment group.\n2. Create a device key for it, then connect it, reporting the platform and architecture of firmware B. When the deployment runs, the device is sent an update for firmware B with its download URL.\n3. To move a device instead, `PUT /api/orgs/\u003cA\u003e/products/\u003cA\u0027s product\u003e/devices/\u003cidentifier\u003e` with `{\"org_id\": \u003cB\u0027s ID\u003e, \"product_id\": \u003cB\u0027s product ID\u003e}`. The response is `201`, and the device is now in B\u0027s product."
                }
              ],
              "value": "As an admin of organization A, with organization B having an active deployment group whose current release is firmware B:\n\n* POST /api/orgs/\u003cA\u003e/products/\u003cA\u0027s product\u003e/devices with {\"identifier\": \"planted\", \"deployment_id\": \u003cB\u0027s deployment group ID\u003e}. The device is created in A, on B\u0027s deployment group.\n* Create a device key for it, then connect it, reporting the platform and architecture of firmware B. When the deployment runs, the device is sent an update for firmware B with its download URL.\n* To move a device instead, PUT /api/orgs/\u003cA\u003e/products/\u003cA\u0027s product\u003e/devices/\u003cidentifier\u003e with {\"org_id\": \u003cB\u0027s ID\u003e, \"product_id\": \u003cB\u0027s product ID\u003e}. The response is 201, and the device is now in B\u0027s product."
            }
          ],
          "x_technicalAnalysis": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003e\u003cstrong\u003e1. Device API and device forms.\u003c/strong\u003e \u003ccode\u003eNervesHubWeb.API.DeviceController.create/2\u003c/code\u003e sets \u003ccode\u003eorg_id\u003c/code\u003e and \u003ccode\u003eproduct_id\u003c/code\u003e from the route, then hands the rest of the request to \u003ccode\u003eDevice.changeset/2\u003c/code\u003e; \u003ccode\u003eupdate/2\u003c/code\u003e hands the whole request over. The changeset casts \u003ccode\u003eorg_id\u003c/code\u003e, \u003ccode\u003eproduct_id\u003c/code\u003e, \u003ccode\u003edeployment_id\u003c/code\u003e, \u003ccode\u003edeleted_at\u003c/code\u003e and the server-managed update state, so an update can move the device into another organization\u0027s product and either request can place it on another organization\u0027s deployment group. The device settings form in \u003ccode\u003eNervesHubWeb.Components.DevicePage.SettingsTab\u003c/code\u003e and the new device form in \u003ccode\u003eNervesHubWeb.Live.Devices.New\u003c/code\u003e reach the same changeset with the same effect.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Signing key and product forms.\u003c/strong\u003e \u003ccode\u003eNervesHubWeb.Live.Org.SigningKeys\u003c/code\u003e and \u003ccode\u003eNervesHubWeb.Live.Products.New\u003c/code\u003e build their parameters with \u003ccode\u003eEnum.into(submitted, %{\"org_id\" =\u0026gt; session_org_id})\u003c/code\u003e, so a submitted \u003ccode\u003eorg_id\u003c/code\u003e overrides the session\u0027s, and the key or product is created in the organization named by the client.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. JITP settings.\u003c/strong\u003e \u003ccode\u003eNervesHub.Devices.CACertificate.JITP.changeset/2\u003c/code\u003e casts any \u003ccode\u003eproduct_id\u003c/code\u003e, through the API and the dashboard, so devices the certificate authority provisions are created in another organization\u0027s product.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4. Firmware delivery.\u003c/strong\u003e When a planted device connects, \u003ccode\u003eNervesHub.ManagedDeployments\u003c/code\u003e checks its deployment group membership by firmware version, platform and architecture only, all reported by the device, so it stays on the foreign group and is sent that group\u0027s current release with a download URL.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "**1. Device API and device forms.** `NervesHubWeb.API.DeviceController.create/2` sets `org_id` and `product_id` from the route, then hands the rest of the request to `Device.changeset/2`; `update/2` hands the whole request over. The changeset casts `org_id`, `product_id`, `deployment_id`, `deleted_at` and the server-managed update state, so an update can move the device into another organization\u0027s product and either request can place it on another organization\u0027s deployment group. The device settings form in `NervesHubWeb.Components.DevicePage.SettingsTab` and the new device form in `NervesHubWeb.Live.Devices.New` reach the same changeset with the same effect.\n\n**2. Signing key and product forms.** `NervesHubWeb.Live.Org.SigningKeys` and `NervesHubWeb.Live.Products.New` build their parameters with `Enum.into(submitted, %{\"org_id\" =\u003e session_org_id})`, so a submitted `org_id` overrides the session\u0027s, and the key or product is created in the organization named by the client.\n\n**3. JITP settings.** `NervesHub.Devices.CACertificate.JITP.changeset/2` casts any `product_id`, through the API and the dashboard, so devices the certificate authority provisions are created in another organization\u0027s product.\n\n**4. Firmware delivery.** When a planted device connects, `NervesHub.ManagedDeployments` checks its deployment group membership by firmware version, platform and architecture only, all reported by the device, so it stays on the foreign group and is sent that group\u0027s current release with a download URL."
                }
              ],
              "value": "1. Device API and device forms. NervesHubWeb.API.DeviceController.create/2 sets org_id and product_id from the route, then hands the rest of the request to Device.changeset/2; update/2 hands the whole request over. The changeset casts org_id, product_id, deployment_id, deleted_at and the server-managed update state, so an update can move the device into another organization\u0027s product and either request can place it on another organization\u0027s deployment group. The device settings form in NervesHubWeb.Components.DevicePage.SettingsTab and the new device form in NervesHubWeb.Live.Devices.New reach the same changeset with the same effect.\n\n2. Signing key and product forms. NervesHubWeb.Live.Org.SigningKeys and NervesHubWeb.Live.Products.New build their parameters with Enum.into(submitted, %{\"org_id\" =\u003e session_org_id}), so a submitted org_id overrides the session\u0027s, and the key or product is created in the organization named by the client.\n\n3. JITP settings. NervesHub.Devices.CACertificate.JITP.changeset/2 casts any product_id, through the API and the dashboard, so devices the certificate authority provisions are created in another organization\u0027s product.\n\n4. Firmware delivery. When a planted device connects, NervesHub.ManagedDeployments checks its deployment group membership by firmware version, platform and architecture only, all reported by the device, so it stays on the foreign group and is sent that group\u0027s current release with a download URL."
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-120730",
        "datePublished": "2026-11-10T10:43:47.924Z",
        "dateReserved": "2026-11-03T03:45:01.695Z",
        "dateUpdated": "2026-11-10T10:43:47.924Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2026-120999 (GCVE-0-2026-120999)

    Vulnerability from cvelistv5 – Published: 2026-11-10 10:43 – Updated: 2026-11-10 10:43
    VLAI
    Title
    Improper authentication lets Google sign-in and confirmation links sign users in to accounts registered with their email address by someone else
    Summary
    Improper Authentication vulnerability in Google sign-in and email confirmation in nerves-hub nerves_hub_web allows an attacker who registers another person's email address to share that person's account once they sign in. NervesHub.Accounts.update_or_create_user_from_ueberauth/1 signs a Google user in to any account carrying the same email that is not yet linked to a Google account, links it and marks the email confirmed, without the account's owner ever having proved they own the address. NervesHubWeb.SessionController.confirm/2 likewise signs in whoever follows a confirmation link. An attacker registers the address from the sign-up form, changes their own account's email to it, or registers it as an organization admin from an invite link, and keeps the password; when the address's owner signs in with Google or follows the confirmation link, they land in that account. The Google path also creates accounts with registrations closed and never checks Google's email_verified claim, and the API's password sign-in in NervesHubWeb.API.UserController issues tokens to accounts whose email was never confirmed. The attacker gains only what the victim is granted afterwards, and nothing on accounts whose owners never use either path. This issue affects nerves_hub: from 2.3.0 before 2.5.3.
    CWE
    • CWE-287 - Improper Authentication
    • CWE-345 - Insufficient Verification of Data Authenticity
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 2.3.0 , < 2.5.3 (other)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 2.3.0 , < 2.5.3 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 74982672ab914bd6f37c3bc0e24a06e3474fa030 , < 54dbf93c3bd323df08223a853c9afcf0b4dbf8e4 (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHub.Accounts\u0027",
                "\u0027Elixir.NervesHub.Accounts.User\u0027",
                "\u0027Elixir.NervesHubWeb.SessionController\u0027",
                "\u0027Elixir.NervesHubWeb.API.UserController\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Account\u0027",
                "\u0027Elixir.NervesHubWeb.AccountController\u0027"
              ],
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io%2Fnerves-hub",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub/accounts.ex",
                "lib/nerves_hub/accounts/user.ex",
                "lib/nerves_hub_web/controllers/session_controller.ex",
                "lib/nerves_hub_web/controllers/api/user_controller.ex",
                "lib/nerves_hub_web/live/account.ex",
                "lib/nerves_hub_web/controllers/account_controller.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:update_or_create_user_from_ueberauth/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:authenticate/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts.User\u0027:oauth_changeset/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:confirm/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:auth/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:login/2"
                }
              ],
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "2.3.0",
                  "versionType": "other"
                }
              ]
            },
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHub.Accounts\u0027",
                "\u0027Elixir.NervesHub.Accounts.User\u0027",
                "\u0027Elixir.NervesHubWeb.SessionController\u0027",
                "\u0027Elixir.NervesHubWeb.API.UserController\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Account\u0027",
                "\u0027Elixir.NervesHubWeb.AccountController\u0027"
              ],
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub/accounts.ex",
                "lib/nerves_hub/accounts/user.ex",
                "lib/nerves_hub_web/controllers/session_controller.ex",
                "lib/nerves_hub_web/controllers/api/user_controller.ex",
                "lib/nerves_hub_web/live/account.ex",
                "lib/nerves_hub_web/controllers/account_controller.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:update_or_create_user_from_ueberauth/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:authenticate/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts.User\u0027:oauth_changeset/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:confirm/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:auth/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:login/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "2.3.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHub.Accounts\u0027",
                "\u0027Elixir.NervesHub.Accounts.User\u0027",
                "\u0027Elixir.NervesHubWeb.SessionController\u0027",
                "\u0027Elixir.NervesHubWeb.API.UserController\u0027",
                "\u0027Elixir.NervesHubWeb.Live.Account\u0027",
                "\u0027Elixir.NervesHubWeb.AccountController\u0027"
              ],
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub/accounts.ex",
                "lib/nerves_hub/accounts/user.ex",
                "lib/nerves_hub_web/controllers/session_controller.ex",
                "lib/nerves_hub_web/controllers/api/user_controller.ex",
                "lib/nerves_hub_web/live/account.ex",
                "lib/nerves_hub_web/controllers/account_controller.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:update_or_create_user_from_ueberauth/1"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts\u0027:authenticate/2"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Accounts.User\u0027:oauth_changeset/3"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.SessionController\u0027:confirm/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:auth/2"
                },
                {
                  "name": "\u0027Elixir.NervesHubWeb.API.UserController\u0027:login/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "54dbf93c3bd323df08223a853c9afcf0b4dbf8e4",
                  "status": "affected",
                  "version": "74982672ab914bd6f37c3bc0e24a06e3474fa030",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.5.3",
                      "versionStartIncluding": "2.3.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "remediation developer",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "coordinator",
              "value": "Jonatan M\u00e4nnchen / EEF"
            }
          ],
          "dateAssigned": "2026-11-09T23:11:54.000Z",
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eImproper Authentication vulnerability in Google sign-in and email confirmation in nerves-hub nerves_hub_web allows an attacker who registers another person\u0027s email address to share that person\u0027s account once they sign in. \u003ccode\u003eNervesHub.Accounts.update_or_create_user_from_ueberauth/1\u003c/code\u003e signs a Google user in to any account carrying the same email that is not yet linked to a Google account, links it and marks the email confirmed, without the account\u0027s owner ever having proved they own the address. \u003ccode\u003eNervesHubWeb.SessionController.confirm/2\u003c/code\u003e likewise signs in whoever follows a confirmation link. An attacker registers the address from the sign-up form, changes their own account\u0027s email to it, or registers it as an organization admin from an invite link, and keeps the password; when the address\u0027s owner signs in with Google or follows the confirmation link, they land in that account.\u003c/p\u003e\n\u003cp\u003eThe Google path also creates accounts with registrations closed and never checks Google\u0027s \u003ccode\u003eemail_verified\u003c/code\u003e claim, and the API\u0027s password sign-in in \u003ccode\u003eNervesHubWeb.API.UserController\u003c/code\u003e issues tokens to accounts whose email was never confirmed. The attacker gains only what the victim is granted afterwards, and nothing on accounts whose owners never use either path.\u003c/p\u003e\n\u003cp\u003eThis issue affects nerves_hub: from 2.3.0 before 2.5.3.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Improper Authentication vulnerability in Google sign-in and email confirmation in nerves-hub nerves_hub_web allows an attacker who registers another person\u0027s email address to share that person\u0027s account once they sign in. `NervesHub.Accounts.update_or_create_user_from_ueberauth/1` signs a Google user in to any account carrying the same email that is not yet linked to a Google account, links it and marks the email confirmed, without the account\u0027s owner ever having proved they own the address. `NervesHubWeb.SessionController.confirm/2` likewise signs in whoever follows a confirmation link. An attacker registers the address from the sign-up form, changes their own account\u0027s email to it, or registers it as an organization admin from an invite link, and keeps the password; when the address\u0027s owner signs in with Google or follows the confirmation link, they land in that account.\n\nThe Google path also creates accounts with registrations closed and never checks Google\u0027s `email_verified` claim, and the API\u0027s password sign-in in `NervesHubWeb.API.UserController` issues tokens to accounts whose email was never confirmed. The attacker gains only what the victim is granted afterwards, and nothing on accounts whose owners never use either path.\n\nThis issue affects nerves_hub: from 2.3.0 before 2.5.3."
                }
              ],
              "value": "Improper Authentication vulnerability in Google sign-in and email confirmation in nerves-hub nerves_hub_web allows an attacker who registers another person\u0027s email address to share that person\u0027s account once they sign in. NervesHub.Accounts.update_or_create_user_from_ueberauth/1 signs a Google user in to any account carrying the same email that is not yet linked to a Google account, links it and marks the email confirmed, without the account\u0027s owner ever having proved they own the address. NervesHubWeb.SessionController.confirm/2 likewise signs in whoever follows a confirmation link. An attacker registers the address from the sign-up form, changes their own account\u0027s email to it, or registers it as an organization admin from an invite link, and keeps the password; when the address\u0027s owner signs in with Google or follows the confirmation link, they land in that account.\n\nThe Google path also creates accounts with registrations closed and never checks Google\u0027s email_verified claim, and the API\u0027s password sign-in in NervesHubWeb.API.UserController issues tokens to accounts whose email was never confirmed. The attacker gains only what the victim is granted afterwards, and nothing on accounts whose owners never use either path.\n\nThis issue affects nerves_hub: from 2.3.0 before 2.5.3."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-114",
              "descriptions": [
                {
                  "lang": "en",
                  "supportingMedia": [
                    {
                      "base64": false,
                      "type": "text/html",
                      "value": "\u003cp\u003eAn attacker who registers someone else\u0027s email address before that person first signs in with Google, or before they follow a confirmation link, ends up sharing that person\u0027s account: the attacker keeps signing in with the password they chose and sees and does whatever the victim can, in every organization the victim joins, at the victim\u0027s role. On an account linked to Google the victim cannot reset the password to lock the attacker out.\u003c/p\u003e"
                    },
                    {
                      "base64": false,
                      "type": "text/markdown",
                      "value": "An attacker who registers someone else\u0027s email address before that person first signs in with Google, or before they follow a confirmation link, ends up sharing that person\u0027s account: the attacker keeps signing in with the password they chose and sees and does whatever the victim can, in every organization the victim joins, at the victim\u0027s role. On an account linked to Google the victim cannot reset the password to lock the attacker out."
                    }
                  ],
                  "value": "An attacker who registers someone else\u0027s email address before that person first signs in with Google, or before they follow a confirmation link, ends up sharing that person\u0027s account: the attacker keeps signing in with the password they chose and sees and does whatever the victim can, in every organization the victim joins, at the victim\u0027s role. On an account linked to Google the victim cannot reset the password to lock the attacker out."
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "PRESENT",
                "attackVector": "NETWORK",
                "baseScore": 7.4,
                "baseSeverity": "HIGH",
                "privilegesRequired": "NONE",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "NONE",
                "subConfidentialityImpact": "NONE",
                "subIntegrityImpact": "NONE",
                "userInteraction": "ACTIVE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
                "version": "4.0",
                "vulnAvailabilityImpact": "NONE",
                "vulnConfidentialityImpact": "HIGH",
                "vulnIntegrityImpact": "HIGH",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-287",
                  "description": "CWE-287 Improper Authentication",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            },
            {
              "descriptions": [
                {
                  "cweId": "CWE-345",
                  "description": "CWE-345 Insufficient Verification of Data Authenticity",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-11-10T10:43:38.941Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "name": "Google sign-in and email confirmation links sign users in to accounts other people registered with their email address",
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-r92p-gg92-gmwm"
            },
            {
              "name": "EEF CNA record for CVE-2026-120999",
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-120999.html"
            },
            {
              "name": "OSV record EEF-CVE-2026-120999",
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-120999"
            },
            {
              "name": "Introducing commit 7498267 in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/74982672ab914bd6f37c3bc0e24a06e3474fa030"
            },
            {
              "name": "Fix commit 54dbf93 in nerves-hub/nerves_hub_web",
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/54dbf93c3bd323df08223a853c9afcf0b4dbf8e4"
            }
          ],
          "solutions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eUpgrading does not undo links already made. Review the \u003ccode\u003eusers\u003c/code\u003e rows that carry both a \u003ccode\u003epassword_hash\u003c/code\u003e and a \u003ccode\u003egoogle_id\u003c/code\u003e; most are one person signing in both ways, but an account registered for someone else\u0027s address was usually created well before its first Google sign-in, so compare \u003ccode\u003einserted_at\u003c/code\u003e with \u003ccode\u003egoogle_last_synced_at\u003c/code\u003e. For one its owner did not set up, set its \u003ccode\u003egoogle_id\u003c/code\u003e to \u003ccode\u003eNULL\u003c/code\u003e, delete its \u003ccode\u003euser_tokens\u003c/code\u003e rows to end its sessions and revoke its API tokens, and have the address\u0027s owner reset the password. An account taken through a confirmation link looks the same as one its owner set up; its owner can end anyone else\u0027s access by resetting the password, which ends every session and revokes every API token. Before upgrading, check how your instance lets people sign up: \u003ccode\u003eSIGN_UP_METHODS\u003c/code\u003e replaces \u003ccode\u003eOPEN_FOR_REGISTRATIONS\u003c/code\u003e, and instances that relied on Google sign-in to create accounts while registrations were off need \u003ccode\u003eSIGN_UP_METHODS=google\u003c/code\u003e.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Upgrading does not undo links already made. Review the `users` rows that carry both a `password_hash` and a `google_id`; most are one person signing in both ways, but an account registered for someone else\u0027s address was usually created well before its first Google sign-in, so compare `inserted_at` with `google_last_synced_at`. For one its owner did not set up, set its `google_id` to `NULL`, delete its `user_tokens` rows to end its sessions and revoke its API tokens, and have the address\u0027s owner reset the password. An account taken through a confirmation link looks the same as one its owner set up; its owner can end anyone else\u0027s access by resetting the password, which ends every session and revokes every API token. Before upgrading, check how your instance lets people sign up: `SIGN_UP_METHODS` replaces `OPEN_FOR_REGISTRATIONS`, and instances that relied on Google sign-in to create accounts while registrations were off need `SIGN_UP_METHODS=google`."
                }
              ],
              "value": "Upgrading does not undo links already made. Review the users rows that carry both a password_hash and a google_id; most are one person signing in both ways, but an account registered for someone else\u0027s address was usually created well before its first Google sign-in, so compare inserted_at with google_last_synced_at. For one its owner did not set up, set its google_id to NULL, delete its user_tokens rows to end its sessions and revoke its API tokens, and have the address\u0027s owner reset the password. An account taken through a confirmation link looks the same as one its owner set up; its owner can end anyone else\u0027s access by resetting the password, which ends every session and revokes every API token. Before upgrading, check how your instance lets people sign up: SIGN_UP_METHODS replaces OPEN_FOR_REGISTRATIONS, and instances that relied on Google sign-in to create accounts while registrations were off need SIGN_UP_METHODS=google."
            }
          ],
          "source": {
            "discovery": "UNKNOWN"
          },
          "title": "Improper authentication lets Google sign-in and confirmation links sign users in to accounts registered with their email address by someone else",
          "workarounds": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eTurn off Google sign-in by unsetting \u003ccode\u003eGOOGLE_CLIENT_ID\u003c/code\u003e until the upgrade; users who only sign in with Google cannot sign in while it is off. This does not close the confirmation link path. Turning off registrations stops strangers using the sign-up form, but not an organization admin registering from an invite link.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Turn off Google sign-in by unsetting `GOOGLE_CLIENT_ID` until the upgrade; users who only sign in with Google cannot sign in while it is off. This does not close the confirmation link path. Turning off registrations stops strangers using the sign-up form, but not an organization admin registering from an invite link."
                }
              ],
              "value": "Turn off Google sign-in by unsetting GOOGLE_CLIENT_ID until the upgrade; users who only sign in with Google cannot sign in while it is off. This does not close the confirmation link path. Turning off registrations stops strangers using the sign-up form, but not an organization admin registering from an invite link."
            }
          ],
          "x_proofOfConcept": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eWith Google sign-in turned on and registrations open:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eRegister an account with \u003ccode\u003evictim@example.com\u003c/code\u003e and a password you choose. Do not confirm it.\u003c/li\u003e\n\u003cli\u003eThe owner of \u003ccode\u003evictim@example.com\u003c/code\u003e signs in to NervesHub with Google for the first time. They are signed in to your account, and its email is now marked confirmed.\u003c/li\u003e\n\u003cli\u003eAn organization invites \u003ccode\u003evictim@example.com\u003c/code\u003e and the victim accepts while signed in.\u003c/li\u003e\n\u003cli\u003eSign in with \u003ccode\u003evictim@example.com\u003c/code\u003e and your password. You are a member of the organization at the role it gave the victim.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eWithout Google sign-in, as an admin of an organization:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eInvite \u003ccode\u003evictim@example.com\u003c/code\u003e, copy the invite link from the members page, and register from it with a password you choose.\u003c/li\u003e\n\u003cli\u003eTry to sign in with \u003ccode\u003evictim@example.com\u003c/code\u003e and your password. NervesHub refuses, since the email is not confirmed, and emails \u003ccode\u003evictim@example.com\u003c/code\u003e a confirmation link.\u003c/li\u003e\n\u003cli\u003eThe victim follows the link and is signed in to your account. Keep using your session or sign in with your password.\u003c/li\u003e\n\u003c/ol\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "With Google sign-in turned on and registrations open:\n\n1. Register an account with `victim@example.com` and a password you choose. Do not confirm it.\n2. The owner of `victim@example.com` signs in to NervesHub with Google for the first time. They are signed in to your account, and its email is now marked confirmed.\n3. An organization invites `victim@example.com` and the victim accepts while signed in.\n4. Sign in with `victim@example.com` and your password. You are a member of the organization at the role it gave the victim.\n\nWithout Google sign-in, as an admin of an organization:\n\n1. Invite `victim@example.com`, copy the invite link from the members page, and register from it with a password you choose.\n2. Try to sign in with `victim@example.com` and your password. NervesHub refuses, since the email is not confirmed, and emails `victim@example.com` a confirmation link.\n3. The victim follows the link and is signed in to your account. Keep using your session or sign in with your password."
                }
              ],
              "value": "With Google sign-in turned on and registrations open:\n\n* Register an account with victim@example.com and a password you choose. Do not confirm it.\n* The owner of victim@example.com signs in to NervesHub with Google for the first time. They are signed in to your account, and its email is now marked confirmed.\n* An organization invites victim@example.com and the victim accepts while signed in.\n* Sign in with victim@example.com and your password. You are a member of the organization at the role it gave the victim.\n\nWithout Google sign-in, as an admin of an organization:\n\n* Invite victim@example.com, copy the invite link from the members page, and register from it with a password you choose.\n* Try to sign in with victim@example.com and your password. NervesHub refuses, since the email is not confirmed, and emails victim@example.com a confirmation link.\n* The victim follows the link and is signed in to your account. Keep using your session or sign in with your password."
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-120999",
        "datePublished": "2026-11-10T10:43:38.941Z",
        "dateReserved": "2026-11-09T22:47:57.298Z",
        "dateUpdated": "2026-11-10T10:43:38.941Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2026-120295 (GCVE-0-2026-120295)

    Vulnerability from cvelistv5 – Published: 2026-11-10 10:43 – Updated: 2026-11-10 10:43
    VLAI
    Title
    Incorrect authorization in the devices page bulk move allows view-role members to move devices into another organization and receive its firmware
    Summary
    Incorrect 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. A 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. This issue affects nerves_hub: from 1.0.0 before 2.5.3.
    CWE
    • CWE-863 - Incorrect Authorization
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (other)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.5.3 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: adaeefdb7a835525482588f43332ef988cc448c7 , < * (git)
    Affected: 79743ef08f7f801617b653fd09f37c01ed9b4be0 , < 1bc8be875ab6b1eb904e5ff60c8a1421c5c700fa (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.Live.Devices.Index\u0027",
                "\u0027Elixir.NervesHub.Products\u0027",
                "\u0027Elixir.NervesHubWeb.Components.BulkActionsSidebar\u0027"
              ],
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io%2Fnerves-hub",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/live/devices/index.ex",
                "lib/nerves_hub/products.ex",
                "lib/nerves_hub_web/components/bulk_actions_sidebar.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Devices.Index\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Products\u0027:get_by_id/2"
                }
              ],
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "other"
                }
              ]
            },
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.Live.Devices.Index\u0027",
                "\u0027Elixir.NervesHub.Products\u0027",
                "\u0027Elixir.NervesHubWeb.Components.BulkActionsSidebar\u0027"
              ],
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/live/devices/index.ex",
                "lib/nerves_hub/products.ex",
                "lib/nerves_hub_web/components/bulk_actions_sidebar.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Devices.Index\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Products\u0027:get_by_id/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.5.3",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "modules": [
                "\u0027Elixir.NervesHubWeb.Live.Devices.Index\u0027",
                "\u0027Elixir.NervesHub.Products\u0027",
                "\u0027Elixir.NervesHubWeb.Components.BulkActionsSidebar\u0027"
              ],
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "programFiles": [
                "lib/nerves_hub_web/live/devices/index.ex",
                "lib/nerves_hub/products.ex",
                "lib/nerves_hub_web/components/bulk_actions_sidebar.ex"
              ],
              "programRoutines": [
                {
                  "name": "\u0027Elixir.NervesHubWeb.Live.Devices.Index\u0027:handle_event/3"
                },
                {
                  "name": "\u0027Elixir.NervesHub.Products\u0027:get_by_id/2"
                }
              ],
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "*",
                  "status": "affected",
                  "version": "adaeefdb7a835525482588f43332ef988cc448c7",
                  "versionType": "git"
                },
                {
                  "lessThan": "1bc8be875ab6b1eb904e5ff60c8a1421c5c700fa",
                  "status": "affected",
                  "version": "79743ef08f7f801617b653fd09f37c01ed9b4be0",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.5.3",
                      "versionStartIncluding": "1.0.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "arbor-s"
            },
            {
              "lang": "en",
              "type": "reporter",
              "value": "arbor-s"
            },
            {
              "lang": "en",
              "type": "remediation developer",
              "value": "Josh Kalderimis"
            },
            {
              "lang": "en",
              "type": "coordinator",
              "value": "Jonatan M\u00e4nnchen / EEF"
            }
          ],
          "dateAssigned": "2026-11-09T22:35:49.000Z",
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eIncorrect Authorization vulnerability in the devices page bulk move in nerves-hub nerves_hub_web allows an authenticated user with the \u003ccode\u003emanage\u003c/code\u003e role in one organization and only the \u003ccode\u003eview\u003c/code\u003e role in another to move devices into the second organization\u0027s product via the \"Move device(s) to product\" action. The \u003ccode\u003etarget-product\u003c/code\u003e event in \u003ccode\u003eNervesHubWeb.Live.Devices.Index\u003c/code\u003e looks the product up with \u003ccode\u003eNervesHub.Products.get_by_id/2\u003c/code\u003e, which checks that the user is a member of the product\u0027s organization at any role, and \u003ccode\u003emove-devices-product\u003c/code\u003e then changes each selected device\u0027s organization and product. The device API\u0027s move requires \u003ccode\u003emanage\u003c/code\u003e in the target organization, and an earlier fix added the check on the organization the devices leave but not on the one they enter.\u003c/p\u003e\n\u003cp\u003eA moved device keeps its key and certificate, joins the target organization\u0027s matching deployment groups, and is sent their firmware with a download URL, which the \u003ccode\u003eview\u003c/code\u003e role cannot otherwise obtain. The user cannot change or control devices that already belong to the other organization.\u003c/p\u003e\n\u003cp\u003eThis issue affects nerves_hub: from 1.0.0 before 2.5.3.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Incorrect 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\u0027s 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\u0027s organization at any role, and `move-devices-product` then changes each selected device\u0027s organization and product. The device API\u0027s 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\u0027s 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."
                }
              ],
              "value": "Incorrect 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\u0027s 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\u0027s organization at any role, and move-devices-product then changes each selected device\u0027s organization and product. The device API\u0027s 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\u0027s 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."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-122",
              "descriptions": [
                {
                  "lang": "en",
                  "supportingMedia": [
                    {
                      "base64": false,
                      "type": "text/html",
                      "value": "\u003cp\u003eA 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.\u003c/p\u003e"
                    },
                    {
                      "base64": false,
                      "type": "text/markdown",
                      "value": "A 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."
                    }
                  ],
                  "value": "A 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."
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "NONE",
                "attackVector": "NETWORK",
                "baseScore": 7.1,
                "baseSeverity": "HIGH",
                "privilegesRequired": "LOW",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "NONE",
                "subConfidentialityImpact": "NONE",
                "subIntegrityImpact": "NONE",
                "userInteraction": "NONE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "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",
                "version": "4.0",
                "vulnAvailabilityImpact": "NONE",
                "vulnConfidentialityImpact": "HIGH",
                "vulnIntegrityImpact": "LOW",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-863",
                  "description": "CWE-863 Incorrect Authorization",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-11-10T10:43:26.342Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "name": "Users with the \u2018view\u2019 role in an organization can move devices into it and receive its firmware",
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-2j7r-q3vm-8j34"
            },
            {
              "name": "EEF CNA record for CVE-2026-120295",
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-120295.html"
            },
            {
              "name": "OSV record EEF-CVE-2026-120295",
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-120295"
            },
            {
              "name": "Introducing commit adaeefd in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/adaeefdb7a835525482588f43332ef988cc448c7"
            },
            {
              "name": "Introducing commit 79743ef in nerves-hub/nerves_hub_web",
              "tags": [
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/79743ef08f7f801617b653fd09f37c01ed9b4be0"
            },
            {
              "name": "Fix commit 1bc8be8 in nerves-hub/nerves_hub_web",
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/1bc8be875ab6b1eb904e5ff60c8a1421c5c700fa"
            }
          ],
          "solutions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eEvery move is recorded in the target product\u0027s audit log as \"User  moved device  to  : \u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "Every move is recorded in the target product\u0027s 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."
                }
              ],
              "value": "Every move is recorded in the target product\u0027s audit log as \"User  moved device  to  : \". 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."
            }
          ],
          "source": {
            "discovery": "UNKNOWN"
          },
          "title": "Incorrect authorization in the devices page bulk move allows view-role members to move devices into another organization and receive its firmware",
          "x_proofOfConcept": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003eAs a user with \u003ccode\u003emanage\u003c/code\u003e in organization A and \u003ccode\u003eview\u003c/code\u003e in organization B, where B has an active deployment group:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eOn A\u0027s devices page, give a device you control the tags B\u0027s deployment group asks for. A \u003ccode\u003eview\u003c/code\u003e member can see the group\u0027s conditions.\u003c/li\u003e\n\u003cli\u003eSelect the device, choose B\u0027s product under \"Move device(s) to product\" and press Move. The page reports the move as successful, and the device is now in B\u0027s product.\u003c/li\u003e\n\u003cli\u003eConnect the device, reporting the platform and architecture of the group\u0027s firmware. It joins the group and is sent B\u0027s firmware with a download URL.\u003c/li\u003e\n\u003c/ol\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "As a user with `manage` in organization A and `view` in organization B, where B has an active deployment group:\n\n1. On A\u0027s devices page, give a device you control the tags B\u0027s deployment group asks for. A `view` member can see the group\u0027s conditions.\n2. Select the device, choose B\u0027s product under \"Move device(s) to product\" and press Move. The page reports the move as successful, and the device is now in B\u0027s product.\n3. Connect the device, reporting the platform and architecture of the group\u0027s firmware. It joins the group and is sent B\u0027s firmware with a download URL."
                }
              ],
              "value": "As a user with manage in organization A and view in organization B, where B has an active deployment group:\n\n* On A\u0027s devices page, give a device you control the tags B\u0027s deployment group asks for. A view member can see the group\u0027s conditions.\n* Select the device, choose B\u0027s product under \"Move device(s) to product\" and press Move. The page reports the move as successful, and the device is now in B\u0027s product.\n* Connect the device, reporting the platform and architecture of the group\u0027s firmware. It joins the group and is sent B\u0027s firmware with a download URL."
            }
          ],
          "x_technicalAnalysis": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "\u003cp\u003e\u003cstrong\u003e1. Target selection.\u003c/strong\u003e The bulk actions sidebar (\u003ccode\u003eNervesHubWeb.Components.BulkActionsSidebar\u003c/code\u003e) lists the products of every organization in \u003ccode\u003ecurrent_scope.user.orgs\u003c/code\u003e. Choosing one sends \u003ccode\u003etarget-product\u003c/code\u003e to \u003ccode\u003eNervesHubWeb.Live.Devices.Index\u003c/code\u003e, which requires \u003ccode\u003edevice:update\u003c/code\u003e in the current organization and then calls \u003ccode\u003eNervesHub.Products.get_by_id/2\u003c/code\u003e. That query joins the product\u0027s organization to its \u003ccode\u003eorg_users\u003c/code\u003e rows and filters on the user\u0027s id only, so any membership in the target organization satisfies it, including the \u003ccode\u003eview\u003c/code\u003e role.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. The move.\u003c/strong\u003e \u003ccode\u003emove-devices-product\u003c/code\u003e passes the selected devices, or every device matching the current filter, to \u003ccode\u003eNervesHub.Devices.BulkActions.move_many/3\u003c/code\u003e, which calls \u003ccode\u003eDevices.move/3\u003c/code\u003e for each one and rewrites its \u003ccode\u003eorg_id\u003c/code\u003e and \u003ccode\u003eproduct_id\u003c/code\u003e. No check of the user\u0027s role in the target organization happens on this path, while the API\u0027s move in \u003ccode\u003eNervesHubWeb.API.DeviceController\u003c/code\u003e requires \u003ccode\u003emanage\u003c/code\u003e there.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. Effect.\u003c/strong\u003e The moved device still authenticates with its own certificate. On its next connection it is matched against the target product\u0027s deployment groups by platform, architecture and tags, joins one, and is sent that group\u0027s firmware with a download URL.\u003c/p\u003e"
                },
                {
                  "base64": false,
                  "type": "text/markdown",
                  "value": "**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\u0027s organization to its `org_users` rows and filters on the user\u0027s 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\u0027s role in the target organization happens on this path, while the API\u0027s 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\u0027s deployment groups by platform, architecture and tags, joins one, and is sent that group\u0027s firmware with a download URL."
                }
              ],
              "value": "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\u0027s organization to its org_users rows and filters on the user\u0027s id only, so any membership in the target organization satisfies it, including the view role.\n\n2. 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\u0027s role in the target organization happens on this path, while the API\u0027s move in NervesHubWeb.API.DeviceController requires manage there.\n\n3. Effect. The moved device still authenticates with its own certificate. On its next connection it is matched against the target product\u0027s deployment groups by platform, architecture and tags, joins one, and is sent that group\u0027s firmware with a download URL."
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-120295",
        "datePublished": "2026-11-10T10:43:26.342Z",
        "dateReserved": "2026-11-06T09:58:06.998Z",
        "dateUpdated": "2026-11-10T10:43:26.342Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2026-28806 (GCVE-0-2026-28806)

    Vulnerability from cvelistv5 – Published: 2026-03-10 21:30 – Updated: 2026-05-27 15:41
    VLAI
    Title
    Improper authorization in device bulk actions and device update API allows cross-organization device control
    Summary
    Improper Authorization vulnerability in nerves-hub nerves_hub_web allows cross-organization device control via device bulk actions and device update API. Missing authorization checks in the device bulk actions and device update API endpoints allow authenticated users to target devices belonging to other organizations and perform actions outside of their privilege level. An attacker can select devices outside of their organization by manipulating device identifiers and perform management actions on them, such as moving them to products they control. This may allow attackers to interfere with firmware updates, access device functionality exposed by the platform, or disrupt device connectivity. In environments where additional features such as remote console access are enabled, this could lead to full compromise of affected devices. This issue affects nerves_hub_web: from 1.0.0 before 2.4.0.
    SSVC
    Exploitation: none Automatable: no Technical Impact: total
    CISA Coordinator · CISA-ADP (v2.0.3)
    Decision recorded 2026-03-11 14:36 UTC
    CWE
    • CWE-285 - Improper Authorization
    • CWE-668 - Exposure of Resource to Wrong Sphere
    Assigner
    EEF CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: 1.0.0 , < 2.4.0 (semver)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    nerves-hub nerves_hub_web Affected: adaeefdb7a835525482588f43332ef988cc448c7 , < 1f69c9d595684a4650c3ac702f3dc7c5bcd7526c (git)
        cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "adp": [
          {
            "metrics": [
              {
                "other": {
                  "content": {
                    "id": "CVE-2026-28806",
                    "options": [
                      {
                        "Exploitation": "none"
                      },
                      {
                        "Automatable": "no"
                      },
                      {
                        "Technical Impact": "total"
                      }
                    ],
                    "role": "CISA Coordinator",
                    "timestamp": "2026-03-11T14:36:05.863739Z",
                    "version": "2.0.3"
                  },
                  "type": "ssvc"
                }
              }
            ],
            "providerMetadata": {
              "dateUpdated": "2026-03-11T14:36:23.357Z",
              "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
              "shortName": "CISA-ADP"
            },
            "title": "CISA ADP Vulnrichment"
          }
        ],
        "cna": {
          "affected": [
            {
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "packageName": "nerves_hub",
              "packageURL": "pkg:otp/nerves_hub?repository_url=https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web\u0026vcs_url=git%20https:%2F%2Fgithub.com%2Fnerves-hub%2Fnerves_hub_web.git",
              "product": "nerves_hub_web",
              "repo": "https://github.com/nerves-hub/nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.4.0",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://ghcr.io",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "packageName": "nerves-hub/nerves-hub",
              "packageURL": "pkg:oci/nerves-hub?repository_url=ghcr.io/nerves-hub",
              "product": "nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "2.4.0",
                  "status": "affected",
                  "version": "1.0.0",
                  "versionType": "semver"
                }
              ]
            },
            {
              "collectionURL": "https://github.com",
              "cpes": [
                "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*"
              ],
              "defaultStatus": "unaffected",
              "packageName": "nerves-hub/nerves_hub_web",
              "packageURL": "pkg:github/nerves-hub/nerves_hub_web",
              "product": "nerves_hub_web",
              "repo": "https://github.com/nerves-hub/nerves_hub_web.git",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "lessThan": "1f69c9d595684a4650c3ac702f3dc7c5bcd7526c",
                  "status": "affected",
                  "version": "adaeefdb7a835525482588f43332ef988cc448c7",
                  "versionType": "git"
                }
              ]
            }
          ],
          "cpeApplicability": [
            {
              "nodes": [
                {
                  "cpeMatch": [
                    {
                      "criteria": "cpe:2.3:a:nerves-hub:nerves_hub_web:*:*:*:*:*:*:*:*",
                      "versionEndExcluding": "2.4.0",
                      "versionStartIncluding": "1.0.0",
                      "vulnerable": true
                    }
                  ],
                  "negate": false,
                  "operator": "OR"
                }
              ],
              "operator": "AND"
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "Josh Kalderimis / NervesHub team \u0026 NervesCloud"
            },
            {
              "lang": "en",
              "type": "analyst",
              "value": "Jonatan M\u00e4nnchen / EEF"
            },
            {
              "lang": "en",
              "type": "remediation reviewer",
              "value": "Lars Wikman / NervesHub team \u0026 NervesCloud"
            }
          ],
          "descriptions": [
            {
              "lang": "en",
              "supportingMedia": [
                {
                  "base64": false,
                  "type": "text/html",
                  "value": "Improper Authorization vulnerability in nerves-hub nerves_hub_web allows cross-organization device control via device bulk actions and device update API.\u003cp\u003eMissing authorization checks in the device bulk actions and device update API endpoints allow authenticated users to target devices belonging to other organizations and perform actions outside of their privilege level.\u003c/p\u003e\u003cp\u003eAn attacker can select devices outside of their organization by manipulating device identifiers and perform management actions on them, such as moving them to products they control. This may allow attackers to interfere with firmware updates, access device functionality exposed by the platform, or disrupt device connectivity.\u003c/p\u003e\u003cp\u003eIn environments where additional features such as remote console access are enabled, this could lead to full compromise of affected devices.\u003c/p\u003e\u003cp\u003eThis issue affects nerves_hub_web: from 1.0.0 before 2.4.0.\u003c/p\u003e"
                }
              ],
              "value": "Improper Authorization vulnerability in nerves-hub nerves_hub_web allows cross-organization device control via device bulk actions and device update API.\n\nMissing authorization checks in the device bulk actions and device update API endpoints allow authenticated users to target devices belonging to other organizations and perform actions outside of their privilege level.\n\nAn attacker can select devices outside of their organization by manipulating device identifiers and perform management actions on them, such as moving them to products they control. This may allow attackers to interfere with firmware updates, access device functionality exposed by the platform, or disrupt device connectivity.\n\nIn environments where additional features such as remote console access are enabled, this could lead to full compromise of affected devices.\n\nThis issue affects nerves_hub_web: from 1.0.0 before 2.4.0."
            }
          ],
          "impacts": [
            {
              "capecId": "CAPEC-1",
              "descriptions": [
                {
                  "lang": "en",
                  "value": "CAPEC-1 Accessing Functionality Not Properly Constrained by ACLs"
                }
              ]
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "Automatable": "NOT_DEFINED",
                "Recovery": "NOT_DEFINED",
                "Safety": "NOT_DEFINED",
                "attackComplexity": "LOW",
                "attackRequirements": "NONE",
                "attackVector": "NETWORK",
                "baseScore": 9.4,
                "baseSeverity": "CRITICAL",
                "exploitMaturity": "NOT_DEFINED",
                "privilegesRequired": "LOW",
                "providerUrgency": "NOT_DEFINED",
                "subAvailabilityImpact": "HIGH",
                "subConfidentialityImpact": "HIGH",
                "subIntegrityImpact": "HIGH",
                "userInteraction": "NONE",
                "valueDensity": "NOT_DEFINED",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
                "version": "4.0",
                "vulnAvailabilityImpact": "HIGH",
                "vulnConfidentialityImpact": "HIGH",
                "vulnIntegrityImpact": "HIGH",
                "vulnerabilityResponseEffort": "NOT_DEFINED"
              },
              "format": "CVSS",
              "scenarios": [
                {
                  "lang": "en",
                  "value": "GENERAL"
                }
              ]
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-285",
                  "description": "CWE-285 Improper Authorization",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            },
            {
              "descriptions": [
                {
                  "cweId": "CWE-668",
                  "description": "CWE-668 Exposure of Resource to Wrong Sphere",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-05-27T15:41:33.000Z",
            "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
            "shortName": "EEF"
          },
          "references": [
            {
              "tags": [
                "vendor-advisory",
                "related"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-f8fr-mccc-xvcx"
            },
            {
              "tags": [
                "related"
              ],
              "url": "https://cna.erlef.org/cves/CVE-2026-28806.html"
            },
            {
              "tags": [
                "related"
              ],
              "url": "https://osv.dev/vulnerability/EEF-CVE-2026-28806"
            },
            {
              "tags": [
                "patch"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/commit/1f69c9d595684a4650c3ac702f3dc7c5bcd7526c"
            }
          ],
          "source": {
            "discovery": "INTERNAL"
          },
          "title": "Improper authorization in device bulk actions and device update API allows cross-organization device control",
          "x_generator": {
            "engine": "Vulnogram 0.2.0"
          }
        }
      },
      "cveMetadata": {
        "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
        "assignerShortName": "EEF",
        "cveId": "CVE-2026-28806",
        "datePublished": "2026-03-10T21:30:58.581Z",
        "dateReserved": "2026-03-03T14:40:00.589Z",
        "dateUpdated": "2026-05-27T15:41:33.000Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }

    CVE-2025-64097 (GCVE-0-2025-64097)

    Vulnerability from cvelistv5 – Published: 2026-01-22 14:57 – Updated: 2026-01-22 16:16
    VLAI
    Title
    NervesHub has Insufficient Token Entropy that Allows Authentication Bypass via Brute Force
    Summary
    NervesHub is a web service that allows users to manage over-the-air (OTA) firmware updates of devices in the field. A vulnerability present starting in version 1.0.0 and prior to version 2.3.0 allowed attackers to brute-force user API tokens due to the predictable format of previously issued tokens. Tokens included user-identifiable components and were not cryptographically secure, making them susceptible to guessing or enumeration. The vulnerability could have allowed unauthorized access to user accounts or API actions protected by these tokens. A fix is available in version 2.3.0 of NervesHub. This version introduces strong, cryptographically-random tokens using `:crypto.strong_rand_bytes/1`, hashing of tokens before database storage to prevent misuse even if the database is compromised, and context-aware token storage to distinguish between session and API tokens. There are no practical workarounds for this issue other than upgrading. In sensitive environments, as a temporary mitigation, firewalling access to the NervesHub server can help limit exposure until an upgrade is possible.
    SSVC
    Exploitation: none Automatable: yes Technical Impact: total
    CISA Coordinator · CISA-ADP (v2.0.3)
    Decision recorded 2026-01-22 16:15 UTC
    CWE
    • CWE-330 - Use of Insufficiently Random Values
    Assigner
    GitHub_M CNA
    Impacted products
    Vendor Product Version
    nerves-hub nerves_hub_web Affected: >= 1.0.0, < 2.3.0
    Create a notification for this product.
    Show details on NVD website

    {
      "containers": {
        "adp": [
          {
            "metrics": [
              {
                "other": {
                  "content": {
                    "id": "CVE-2025-64097",
                    "options": [
                      {
                        "Exploitation": "none"
                      },
                      {
                        "Automatable": "yes"
                      },
                      {
                        "Technical Impact": "total"
                      }
                    ],
                    "role": "CISA Coordinator",
                    "timestamp": "2026-01-22T16:15:56.743700Z",
                    "version": "2.0.3"
                  },
                  "type": "ssvc"
                }
              }
            ],
            "providerMetadata": {
              "dateUpdated": "2026-01-22T16:16:06.007Z",
              "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
              "shortName": "CISA-ADP"
            },
            "title": "CISA ADP Vulnrichment"
          }
        ],
        "cna": {
          "affected": [
            {
              "product": "nerves_hub_web",
              "vendor": "nerves-hub",
              "versions": [
                {
                  "status": "affected",
                  "version": "\u003e= 1.0.0, \u003c 2.3.0"
                }
              ]
            }
          ],
          "descriptions": [
            {
              "lang": "en",
              "value": "NervesHub is a web service that allows users to manage over-the-air (OTA) firmware updates of devices in the field. A vulnerability present starting in version 1.0.0 and prior to version 2.3.0 allowed attackers to brute-force user API tokens due to the predictable format of previously issued tokens. Tokens included user-identifiable components and were not cryptographically secure, making them susceptible to guessing or enumeration. The vulnerability could have allowed unauthorized access to user accounts or API actions protected by these tokens. A fix is available in version 2.3.0 of NervesHub. This version introduces strong, cryptographically-random tokens using `:crypto.strong_rand_bytes/1`, hashing of tokens before database storage to prevent misuse even if the database is compromised, and context-aware token storage to distinguish between session and API tokens. There are no practical workarounds for this issue other than upgrading. In sensitive environments, as a temporary mitigation,\nfirewalling access to the NervesHub server can help limit exposure until an upgrade is possible."
            }
          ],
          "metrics": [
            {
              "cvssV4_0": {
                "attackComplexity": "LOW",
                "attackRequirements": "PRESENT",
                "attackVector": "NETWORK",
                "baseScore": 9.5,
                "baseSeverity": "CRITICAL",
                "privilegesRequired": "NONE",
                "subAvailabilityImpact": "HIGH",
                "subConfidentialityImpact": "HIGH",
                "subIntegrityImpact": "HIGH",
                "userInteraction": "NONE",
                "vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:H",
                "version": "4.0",
                "vulnAvailabilityImpact": "NONE",
                "vulnConfidentialityImpact": "HIGH",
                "vulnIntegrityImpact": "HIGH"
              }
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-330",
                  "description": "CWE-330: Use of Insufficiently Random Values",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-01-22T14:57:00.362Z",
            "orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
            "shortName": "GitHub_M"
          },
          "references": [
            {
              "name": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-m9vj-776q-vc8m",
              "tags": [
                "x_refsource_CONFIRM"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/security/advisories/GHSA-m9vj-776q-vc8m"
            },
            {
              "name": "https://github.com/nerves-hub/nerves_hub_web/pull/2024",
              "tags": [
                "x_refsource_MISC"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/pull/2024"
            },
            {
              "name": "https://github.com/nerves-hub/nerves_hub_web/releases/tag/v2.3.0",
              "tags": [
                "x_refsource_MISC"
              ],
              "url": "https://github.com/nerves-hub/nerves_hub_web/releases/tag/v2.3.0"
            }
          ],
          "source": {
            "advisory": "GHSA-m9vj-776q-vc8m",
            "discovery": "UNKNOWN"
          },
          "title": "NervesHub has Insufficient Token Entropy that Allows Authentication Bypass via Brute Force"
        }
      },
      "cveMetadata": {
        "assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
        "assignerShortName": "GitHub_M",
        "cveId": "CVE-2025-64097",
        "datePublished": "2026-01-22T14:57:00.362Z",
        "dateReserved": "2025-10-27T15:26:14.126Z",
        "dateUpdated": "2026-01-22T16:16:06.007Z",
        "state": "PUBLISHED"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }