Skip to content

Commit 76a6bfd

Browse files
committed
Fix NullPointerException decoding an ACI bind rule with a missing and/or operand (#719)
An empty group such as "()" decodes to a null bind rule. BindRule.decode dereferenced that null when it appeared as the left or right operand of an "and"/"or" bind rule (e.g. "(()or)"), throwing a NullPointerException instead of an AciException. Reject the missing operand - and a bare "()" group - with an AciException.
1 parent 2714791 commit 76a6bfd

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/BindRule.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ else if (!inQuotes && currentChar == ')')
206206
bindruleStr.substring(currentPos + 1);
207207
return createBindRule(bindrule_1, remainingBindruleStr);
208208
}
209+
/*
210+
* An empty group such as "()" decodes to a null bind rule. A group
211+
* with no bind rule inside is not valid, so reject it instead of
212+
* returning null and letting the caller dereference it later.
213+
*/
214+
if (bindrule_1 == null) {
215+
throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input));
216+
}
209217
return bindrule_1;
210218
}
211219
else
@@ -289,6 +297,15 @@ private static BindRule parseAndCreateBindrule(Matcher bindruleMatcher) throws A
289297
*/
290298
private static BindRule createBindRule(BindRule bindrule,
291299
String remainingBindruleStr) throws AciException {
300+
/*
301+
* The left operand of an "and"/"or" bind rule must exist. It is null when
302+
* the left side reduced to an empty group such as "()" (for example the
303+
* bind rule "()or userdn=..."). A complex bind rule with a null left side
304+
* would later be dereferenced during evaluation, so reject it here.
305+
*/
306+
if (bindrule == null) {
307+
throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr));
308+
}
292309
Pattern remainingBindrulePattern = Pattern.compile(remainingBindruleRegex);
293310
Matcher remainingBindruleMatcher = remainingBindrulePattern.matcher(remainingBindruleStr);
294311
if (remainingBindruleMatcher.find()) {
@@ -312,6 +329,15 @@ private static BindRule createBindRule(BindRule bindrule,
312329
boolean negate=determineNegation(ruleExpr);
313330
remainingBindrule=ruleExpr.toString();
314331
BindRule bindrule_2 = BindRule.decode(remainingBindrule);
332+
/*
333+
* The right operand of an "and"/"or" bind rule must exist. It is null
334+
* when the boolean operator is not followed by a bind rule, e.g. the
335+
* "or" in "(()or)" has nothing to its right (issue #719).
336+
*/
337+
if (bindrule_2 == null) {
338+
throw new AciException(
339+
WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr));
340+
}
315341
bindrule_2.setNegate(negate);
316342
return new BindRule(bindrule, bindrule_2, operand);
317343
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
package org.opends.server.authorization.dseecompat;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.forgerock.opendj.ldap.ByteString;
21+
import org.forgerock.opendj.ldap.DN;
22+
import org.opends.server.DirectoryServerTestCase;
23+
import org.opends.server.TestCaseUtils;
24+
import org.opends.server.types.DirectoryException;
25+
import org.testng.annotations.AfterClass;
26+
import org.testng.annotations.BeforeClass;
27+
import org.testng.annotations.DataProvider;
28+
import org.testng.annotations.Test;
29+
30+
/**
31+
* Verifies that {@link BindRule#decode(String)} rejects a boolean ("and"/"or")
32+
* bind rule that is missing one of its operands, instead of throwing a
33+
* {@link NullPointerException}. An empty group such as "()" decodes to a null
34+
* bind rule; before this fix, using such a group as the left side, or leaving
35+
* the right side of the boolean empty (e.g. "(()or)"), dereferenced that null
36+
* while parsing and failed with an NPE rather than a diagnosable
37+
* {@link AciException} (issue #719).
38+
*/
39+
@SuppressWarnings("javadoc")
40+
public class BindRuleOperandTest extends DirectoryServerTestCase
41+
{
42+
@BeforeClass
43+
public void setUp() throws Exception
44+
{
45+
TestCaseUtils.startFakeServer();
46+
}
47+
48+
@AfterClass
49+
public void tearDown() throws DirectoryException
50+
{
51+
TestCaseUtils.shutdownFakeServer();
52+
}
53+
54+
@DataProvider(name = "bindRulesWithMissingOperand")
55+
public Object[][] bindRulesWithMissingOperand()
56+
{
57+
return new Object[][] {
58+
// the exact bind rule from issue #719: "or" with no right operand and an
59+
// empty left group
60+
{ "(()or)" },
61+
// empty right operand after a valid left operand
62+
{ "(userdn=\"ldap:///self\" or)" },
63+
{ "(userdn=\"ldap:///self\" and)" },
64+
// empty left operand before a valid right operand
65+
{ "()or userdn=\"ldap:///self\"" },
66+
// an empty group is not a bind rule on its own
67+
{ "()" },
68+
};
69+
}
70+
71+
@Test(dataProvider = "bindRulesWithMissingOperand")
72+
public void rejectsMissingOperand(String bindRule)
73+
{
74+
assertThatThrownBy(() -> BindRule.decode(bindRule)).isInstanceOf(AciException.class);
75+
}
76+
77+
/** Reproduces the full ACI from issue #719 through {@link Aci#decode}. */
78+
@Test
79+
public void decodeOfAciWithMissingOperandThrowsAciException()
80+
{
81+
String aci = "(version 3.0; acl \"ac\"; allow (search)(()or) (userdn=\"ldap:///self\"); )";
82+
assertThatThrownBy(() -> Aci.decode(ByteString.valueOfUtf8(aci), DN.rootDN()))
83+
.isInstanceOf(AciException.class);
84+
}
85+
}

0 commit comments

Comments
 (0)