Skip to content

Fix CodeQL warnings in the bundled scripts - #209

Open
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:fix-codeql-warnings-scripts
Open

vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:fix-codeql-warnings-scripts

Conversation

@vharseko

Copy link
Copy Markdown
Member

Summary

Third warning-level CodeQL batch: the JavaScript findings in the bundled server-side scripts (openidm-zip). 19 alerts fixed, 10 dismissed as false positives. The 7 alerts in policy.js (#768–#770, #782, #783, #895, #896) are deferred until #204, which rewrites part of that file, has landed.

Three real bugs (bug below); the rest are scoping fixes.

Alerts File Change
#759/#760 index-out-of-bounds — bug router-authz.js contains / containsIgnoreCase looped i <= a.length, reading the slot past the end. contains(list, undefined) and containsIgnoreCase([], undefined) therefore returned true. Now <. New routerAuthzTest.js evaluates the real script with stubbed host globals (the function declarations are hoisted, so they stay reachable after the trailing access check throws) — 8 cases, 2 failed before the fix.
#907 comparison-between-incompatible-types — bug ui/correlateTreeToQueryFilter.js typeof linkQualifier !== undefined is always true; now compares against "undefined". No behavioural change in real invocations: the sync engine (Correlation.java) and the admin UI always bind linkQualifier.
#785 use-before-declaration — bug roles/effectiveAssignments.js The effectiveRolesPropName = "effectiveRoles" default was applied after object[effectiveRolesPropName] had already been read, so without an explicit globals entry in managed.json the script read object[undefined]. The default now precedes the first use.
#771 missing-variable-declaration audit/autoPurgeAuditRecon.js Declared excludeMappings, used excludeMapping — typo.
#779/#780 missing-variable-declaration roles/temporalConstraints.js var constraintExpired = false; — the ; ended the declaration list, so dateUtil became a global.
#772–#778, #781, #784 missing-variable-declaration policyFilter.js, defaultMapping.js, effectiveRoles.js, postOperation-roles.js ×4, relationshipHelper.js, samples/multiplepasswords/script/pwpolicy.js Missing var.
#786 use-before-declaration roles/defaultMapping.js Two block-level var config = getConfig(…) were hoisted to script scope and overwrote the config binding (the mapping configuration) after it had been read; renamed to unassignmentConfig / assignmentConfig.
#787 use-before-declaration samples/usecase/script/roles/effectiveRoles.js Default expressed as var rolesPropName = rolesPropName === undefined ? "roles" : rolesPropName;.
#749 unreachable-statement info/login.js Dropped the return val after the if/else that always returns or throws.

Dismissed as false positives: #899–#906 useless-expression — the trailing bare expression is the Rhino idiom for a script's return value (a top-level return is a syntax error in a script); #893/#894 useless-assignment-to-local — source is read by the transform script evaluated through eval(p.transform.source) two lines later.

Test plan

  • routerAuthzTest.js (new): contains(["a","b"], undefined) and containsIgnoreCase([], undefined) returned true before, false now; positive/negative cases pass
  • mvn -pl openidm-zip -am package — ScriptRunnerTest green over all 9 JS test modules, including effectiveRolesTest, temporalConstraintsTest and conditionalRolesTest which exercise the edited role scripts
  • CodeQL on this PR closes #749, #759, #760, #771–#781, #784–#787, #907

- router-authz.js: the list helpers iterated one past the end, so
  contains(list, undefined) and containsIgnoreCase([], undefined) were true
- correlateTreeToQueryFilter.js: compare typeof against the string
  "undefined", not the value
- effectiveAssignments.js: apply the effectiveRolesPropName default before
  it is used to read the object
- Declare the locals that were leaking into the global scope (a typo in
  autoPurgeAuditRecon.js, a ';' that ended a var list early in
  temporalConstraints.js, missing var elsewhere) and stop re-declaring the
  mapping-config binding in defaultMapping.js
- Remove the unreachable return in info/login.js

Resolves CodeQL alerts #749, #759, #760, #771-#781, #784-#787, #907.
@vharseko vharseko added javascript Pull requests that update Javascript code bug Something isn't working test Tests and test infrastructure (unit, e2e, smoke) samples Sample configurations and use cases labels Sep 18, 2026

@maximthomas maximthomas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: The CodeQL fixes target the actual bugs, and the off-by-one comes with a test that runs the real script.

  • contains / containsIgnoreCase now loop i < a.length (router-authz.js:62, :105), and routerAuthzTest.js evaluates the shipped router-authz.js instead of a copy. Two of its 8 cases fail at the base commit.
  • In roles/effectiveAssignments.js:37 the effectiveRolesPropName default is now applied before object[effectiveRolesPropName] is read at :40.

issue (blocking): With the off-by-one fixed, isQueryOneOf returns false for every patch-by-query request, so the openidm-cert rule never matches.

openidm-zip/src/main/resources/bin/defaults/script/router-authz.js:203-212, openidm-zip/src/main/resources/script/access.js:325-330

isQueryOneOf is used in one place: the openidm-cert rule (managed/user, methods patch,action, actions patch). Password-sync plugins hit that rule with POST managed/user?_action=patch&_queryId=for-userName&uid=… (documented in chap-passwords.adoc:897 and appendix-rest.adoc:840). That call is an ActionRequest. Its script wrapper (ScriptableActionRequest → AbstractScriptableRequest, commons script javascript 3.1.2) has no queryId property, so request.queryId is undefined. _queryId only shows up in request.additionalParameters, and ManagedObjectSet.patchAction builds its query from those parameters too.

  • At the base commit, contains(['for-userName'], undefined) matched the slot past the end of the list, so the rule accepted every patch-by-query, including _queryFilter.
  • At this head the same call returns false, so a cert-authenticated password sync gets 403.

The PR closes the bypass, but it also blocks the call the rule was written for, and the description does not mention any authorization change. Keep the loop fix and read _queryId from the additional parameters:

function isQueryOneOf(allowedQueries) {
    // patch-by-query is an action request: its _queryId is an additional parameter
    var queryId = request.queryId;
    if ((queryId === undefined || queryId === null) && request.additionalParameters) {
        queryId = request.additionalParameters._queryId;
    }
    return !!allowedQueries[request.resourcePath] &&
            contains(allowedQueries[request.resourcePath], queryId);
}

Pin: in routerAuthzTest.js, change loadHelpers to loadHelpers(source, request) so it takes the stub request (default { method: "read", resourcePath: "info/ping" }) and also returns isQueryOneOf. Then add the cases below. Case 1 fails at this head without the fix; case 2 fails at the base commit.

[
    // additionalParameters of POST managed/user?_action=patch, expected, scenario
    [{ _queryId: "for-userName", uid: "DDOE" }, true, "patch-by-query with the allowed _queryId"],
    [{ _queryFilter: "true" }, false, "patch-by-query without _queryId"],
    [{ _queryId: "query-all-ids" }, false, "patch-by-query with another _queryId"]
].forEach(function (testcase) {
    var actual = loadHelpers(source, { method: "action", action: "patch", resourcePath: "managed/user",
            additionalParameters: testcase[0], content: [] })
        .isQueryOneOf({ "managed/user": ["for-userName"] });
    if (actual !== testcase[1]) {
        throw { "message": "isQueryOneOf: " + testcase[2] + " - got <" + actual + ">, expected <" + testcase[1] + ">" };
    }
});

suggestion (non-blocking): The effectiveRolesPropName reorder is one of the three bug fixes in the description, and no test covers it.

openidm-zip/src/main/resources/bin/defaults/script/roles/effectiveAssignments.js:36-40

effectiveAssignments.js is not loaded by any test (testRunner.js runs 8 other modules), and every bundled managed.json sets effectiveRolesPropName. If the default were moved back below :40, every test would still pass. The typeof fix in ui/correlateTreeToQueryFilter.js:50 is in the same position. Up to you; here is a test that fails at the base commit (object[undefined] → []) and passes at this head:

// effectiveAssignmentsTest.js, added to testRunner.js; readClasspathResource as in routerAuthzTest.js
function evalEffectiveAssignments(source) {
    var object = { effectiveRoles: [ { _ref: "managed/role/r1" } ] },
        context = {},
        propertyName = "effectiveAssignments",
        logger = { debug: function () {}, trace: function () {} },
        openidm = { read: function (id) {
            return id === "managed/role/r1"
                ? { assignments: [ { _ref: "managed/assignment/a1" } ] }
                : { _id: "a1" };
        } };
    return eval(source);   // no effectiveRolesPropName binding: the default must apply
}
if (evalEffectiveAssignments(readClasspathResource(
        "bin/defaults/script/roles/effectiveAssignments.js")).length !== 1) {
    throw { "message": "effectiveRolesPropName default not applied before object[effectiveRolesPropName] is read" };
}

suggestion (non-blocking): No test runs roles/defaultMapping.js, so the config → unassignmentConfig / assignmentConfig rename is not covered by any test.

openidm-zip/src/main/resources/bin/defaults/script/roles/defaultMapping.js:204-209, :270-275

The rename preserves behaviour. However, if one use site still passed config (for example :275), the mapping configuration, which has no file or source, would reach openidm.action("script", "eval", …). Role-assignment sync would then break at runtime and every test would still pass. This gap existed before the PR, and a test for a pure rename is of low value, so this is up to you.

Pin: a defaultMappingTest.js that runs the script with stubbed source / target / config / openidm.action and asserts that the assignmentOperation config, with attributeName and attributeValue set, is what reaches openidm.action("script", "eval", …).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working javascript Pull requests that update Javascript code samples Sample configurations and use cases test Tests and test infrastructure (unit, e2e, smoke)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants