Skip to content

Commit 886e7e4

Browse files
committed
CVE-2026-62961 Query-filter injection in bundled auth augmentation scripts
1 parent 4639451 commit 886e7e4

5 files changed

Lines changed: 158 additions & 3 deletions

File tree

openidm-zip/src/main/resources/bin/defaults/script/auth/amSessionCheck.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* information: "Portions copyright [year] [name of copyright owner]".
1313
*
1414
* Copyright 2016 ForgeRock AS.
15+
* Portions copyright 2026 3A Systems, LLC
1516
*/
1617
var base64 = Packages.org.forgerock.util.encode.Base64url,
1718
id_token = (httpRequest.getHeaders().getFirst('authToken').toString()+""),
@@ -56,8 +57,11 @@ if (security.authenticationId === "amadmin") {
5657
"moduleId" : security.authorization.moduleId
5758
};
5859
} else if (security.authorization.component !== "managed/user") {
60+
// Escape the untrusted authenticationId so it cannot break out of the query filter string
61+
// literal and inject additional predicates (e.g. ' or /userName eq "victim').
5962
var _ = require('lib/lodash'),
60-
managedUser = openidm.query("managed/user", { '_queryFilter' : '/userName eq "' + security.authenticationId + '"' }, ["*","authzRoles"]);
63+
queryFilter = require('auth/queryFilter'),
64+
managedUser = openidm.query("managed/user", { '_queryFilter' : '/userName eq "' + queryFilter.escapeStringValue(security.authenticationId) + '"' }, ["*","authzRoles"]);
6165

6266
if (managedUser.result.length === 0) {
6367
throw {

openidm-zip/src/main/resources/bin/defaults/script/auth/populateAsManagedUser.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11

2+
/*
3+
* The contents of this file are subject to the terms of the Common Development and
4+
* Distribution License (the License). You may not use this file except in compliance with the
5+
* License.
6+
*
7+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
8+
* specific language governing permission and limitations under the License.
9+
*
10+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
11+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
12+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
13+
* information: "Portions copyright [year] [name of copyright owner]".
14+
*
15+
* Portions copyright 2026 3A Systems, LLC
16+
*/
17+
218
/*global security, properties, openidm */
319

420

@@ -39,6 +55,7 @@
3955
logger.debug("Augment context for: {}", security.authenticationId);
4056

4157
var _ = require("lib/lodash"),
58+
queryFilter = require("auth/queryFilter"),
4259
userDetail,
4360
resource = properties.queryOnResource,
4461
propertyMapping = properties.propertyMapping,
@@ -47,7 +64,9 @@
4764
managedUser;
4865

4966

50-
managedUser = openidm.query("managed/user", { '_queryFilter' : '/userName eq "' + security.authenticationId + '"' }, ["*","authzRoles"]);
67+
// Escape the untrusted authenticationId so it cannot break out of the query filter string
68+
// literal and inject additional predicates (e.g. ' or /userName eq "victim').
69+
managedUser = openidm.query("managed/user", { '_queryFilter' : '/userName eq "' + queryFilter.escapeStringValue(security.authenticationId) + '"' }, ["*","authzRoles"]);
5170

5271
if (managedUser.result.length === 0) {
5372
throw {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2026 3A Systems, LLC
15+
*/
16+
17+
/*global exports */
18+
19+
/**
20+
* Helpers for building CREST query filters safely from untrusted input.
21+
*/
22+
(function () {
23+
24+
/**
25+
* Escapes a value so it can be safely embedded as a double-quoted string literal inside a
26+
* CREST query filter, e.g. <code>/userName eq "&lt;value&gt;"</code>.
27+
*
28+
* Inside such a literal only the backslash and the double-quote characters are syntactically
29+
* significant, so escaping them prevents a crafted value from closing the literal early and
30+
* injecting additional predicates. Without this, an authenticationId such as
31+
* <code>zzz" or /userName eq "victim</code> would turn the lookup into
32+
* <code>/userName eq "zzz" or /userName eq "victim"</code> and match an unintended account.
33+
*
34+
* The backslash must be escaped before the double-quote so that an input ending in a
35+
* backslash cannot escape the closing delimiter.
36+
*
37+
* @param {*} value the raw value (coerced to a string)
38+
* @returns {string} the value with backslash and double-quote characters backslash-escaped
39+
*/
40+
exports.escapeStringValue = function (value) {
41+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
42+
};
43+
44+
}());
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2026 3A Systems, LLC
15+
*/
16+
17+
/**
18+
* Tests against auth/queryFilter.js, the helper used by the authentication augmentation scripts
19+
* (auth/populateAsManagedUser.js and auth/amSessionCheck.js) to safely embed an untrusted
20+
* authenticationId into a managed/user query filter (GHSA-64q4-cp2r-m7rg).
21+
*/
22+
exports.test = function () {
23+
var queryFilter = require("auth/queryFilter");
24+
25+
testEscapeStringValue();
26+
testInjectionIsNeutralized();
27+
28+
function testEscapeStringValue() {
29+
// [ rawValue, expectedEscapedValue ]
30+
[
31+
// ordinary identifiers are untouched
32+
["jsmith", "jsmith"],
33+
["", ""],
34+
["user@example.com", "user@example.com"],
35+
// the GHSA-64q4-cp2r-m7rg payload: the closing quote is escaped so it can no longer
36+
// terminate the literal and start a second predicate
37+
['zzz" or /userName eq "victim', 'zzz\\" or /userName eq \\"victim'],
38+
// a bare double-quote is escaped
39+
['a"b', 'a\\"b'],
40+
// a backslash is escaped (and must be escaped before quotes)
41+
["a\\b", "a\\\\b"],
42+
// a trailing backslash cannot escape the closing delimiter once doubled
43+
["foo\\", "foo\\\\"],
44+
// backslash immediately before a quote: both are escaped independently
45+
['a\\"b', 'a\\\\\\"b']
46+
].map(function (testcase) {
47+
(function (rawValue, expectedEscapedValue) {
48+
var escaped = queryFilter.escapeStringValue(rawValue) + "";
49+
if (escaped !== expectedEscapedValue) {
50+
throw {
51+
"message": "escapeStringValue(<" + rawValue + ">) returned <" + escaped
52+
+ ">, expected <" + expectedEscapedValue + ">"
53+
};
54+
}
55+
}).apply(null, testcase);
56+
});
57+
}
58+
59+
/**
60+
* Verifies that, once escaped, the value contributes no syntactically significant (unescaped)
61+
* double-quote to the filter, so the only unescaped quotes are the two delimiters the script
62+
* adds around the value. This is what prevents the injected
63+
* <code>" or /userName eq "victim</code> from becoming a second predicate.
64+
*/
65+
function testInjectionIsNeutralized() {
66+
[
67+
"jsmith",
68+
'zzz" or /userName eq "victim',
69+
'a"b"c',
70+
"trailingBackslash\\",
71+
'mixed\\"quote'
72+
].map(function (rawValue) {
73+
var filter = '/userName eq "' + queryFilter.escapeStringValue(rawValue) + '"';
74+
// Remove every escaped pair (\\ or \") so that only structural characters remain;
75+
// any double-quote left is then an actual filter delimiter.
76+
var structural = filter.replace(/\\[\\"]/g, "");
77+
var unescapedQuotes = (structural.match(/"/g) || []).length;
78+
if (unescapedQuotes !== 2) {
79+
throw {
80+
"message": "filter for <" + rawValue + "> has " + unescapedQuotes
81+
+ " unescaped quote(s), expected exactly 2 delimiters; filter was <" + filter + ">"
82+
};
83+
}
84+
});
85+
}
86+
};

openidm-zip/src/test/resources/testRunner.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
* information: "Portions copyright [year] [name of copyright owner]".
1313
*
1414
* Copyright 2016 ForgeRock AS.
15+
* Portions copyright 2026 3A Systems, LLC
1516
*/
1617

1718
/**
1819
* Backend script module test runner. For each module to be tested, create a suitable *Test module that
1920
* exports a "test" method, and add it to the array of test modules below.
2021
*/
21-
[ "policyFilterTest",
22+
[ "policyFilterTest",
23+
"queryFilterTest",
2224
"effectiveRolesTest",
2325
"temporalConstraintsTest",
2426
"conditionalRolesTest",

0 commit comments

Comments
 (0)