Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,9 @@ public abstract class CelEnvironment {
"cel.limit.parse_error_recovery",
CelOptions.Builder::maxParseErrorRecoveryLimit,
"cel.limit.parse_recursion_depth",
CelOptions.Builder::maxParseRecursionDepth);
CelOptions.Builder::maxParseRecursionDepth,
"cel.limit.expression_node_count",
CelOptions.Builder::maxParseExpressionNodeCount);

private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
ImmutableMap.of(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_node_count", options.maxParseExpressionNodeCount()));
}
envBuilder.setLimits(limits.build());
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,6 +348,7 @@ public void options() {
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.maxParseExpressionNodeCount(500)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
Expand All@@ -365,6 +366,7 @@ public void options() {
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
}
}
25 changes: 24 additions & 1 deletion bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception {
.setLimits(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
.build();

Cel cel =
Expand All@@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception {
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);

CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
Long result = (Long) cel.createProgram(ast).eval();
Expand All@@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception {
.contains("expression code point size exceeds limit: size: 21, limit 20");
}

@Test
public void extend_expressionNodeCountLimit() throws Exception {
CelEnvironment environment =
CelEnvironment.newBuilder()
.setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
.build();

Cel cel =
environment.extend(
CelFactory.legacyCelBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.build(),
CelOptions.DEFAULT);
CelOptions checkerOptions = cel.toCheckerBuilder().options();
assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);

CelValidationResult validationResult = cel.compile("1 + 2 + 3");
assertThat(validationResult.hasError()).isTrue();
assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
}

@Test
public void extend_unsupportedFeatureFlag_throws() throws Exception {
CelEnvironment environment =
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions {

public abstract int maxParseRecursionDepth();

public abstract int maxParseExpressionNodeCount();

public abstract boolean populateMacroCalls();

public abstract boolean retainRepeatedUnaryOperators();
Expand DownExpand Up@@ -134,6 +136,7 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
.maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
Expand DownExpand Up@@ -223,6 +226,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);

/**
* Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
* This prevents cases where macro expansion results in an AST that is larger than expected from
* the source expression. Once exceeded, the parser will record an error and stop expanding
* macros but continue parsing to report other errors.
*/
public abstract Builder maxParseExpressionNodeCount(int value);

/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);

Expand Down
50 changes: 37 additions & 13 deletions parser/src/main/java/dev/cel/parser/Parser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti
new ExprFactory(
antlrParser,
sourceInfo,
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME);
options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME,
options.maxParseExpressionNodeCount());
Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory);
ErrorListener errorListener = new ErrorListener(exprFactory);
antlrLexer.removeErrorListeners();
Expand DownExpand Up@@ -655,6 +656,12 @@ private Optional<CelExpr> visitMacro(
ImmutableList<CelExpr> args,
Optional<CelExpr> target,
CelMacro macro) {
if (exprFactory.isNodeLimitExceeded()) {
return Optional.of(
exprFactory.reportError(
exprFactory.getPosition(expr.id()),
"could not expand macro: expression node limit exceeded"));
}

Optional<CelExpr> expandedMacro =
expandMacro(
Expand DownExpand Up@@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory {
private final ArrayList<CelIssue> issues;
private final ArrayDeque<Integer> positions;
private final String accumulatorVarName;
private final int maxExpressionNodeCount;
private boolean nodeLimitExceeded;

private ExprFactory(
org.antlr.v4.runtime.Parser recognizer,
CelSource.Builder sourceInfo,
String accumulatorVarName) {
String accumulatorVarName,
int maxExpressionNodeCount) {
this.recognizer = recognizer;
this.sourceInfo = sourceInfo;
this.issues = new ArrayList<>();
this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position.
this.accumulatorVarName = accumulatorVarName;
this.maxExpressionNodeCount = maxExpressionNodeCount;
}

// Implementation of CelExprFactory.
Expand All@@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) {
return ERROR;
}

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

// Internal methods used by the parser but not part of the public API.
@FormatMethod
@CanIgnoreReturnValue
private CelExpr reportError(
Expand All@@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) {
return reportError(CelIssue.formatError(getLocation(token), message));
}

@CanIgnoreReturnValue
private CelExpr reportError(int position, String message) {
return reportError(CelIssue.formatError(getLocation(position), message));
}

// Implementation of CelExprFactory.

@Override
public String getAccumulatorVarName() {
return accumulatorVarName;
}

@Override
protected CelSourceLocation currentSourceLocationForMacro() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
Expand All@@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() {

// Internal methods used by the parser but not part of the public API.

private boolean isNodeLimitExceeded() {
return nodeLimitExceeded;
}

private void pushPosition(int position) {
positions.addLast(position);
}
Expand All@@ -1159,24 +1178,29 @@ private int peekPosition() {

private long nextExprId(int position) {
long exprId = super.nextExprId();
if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) {
nodeLimitExceeded = true;
reportError(
position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount));
}
if (position != -1) {
sourceInfo.addPositions(exprId, position);
}
return exprId;
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

@Override
public long nextExprId() {
checkState(!positions.isEmpty()); // Should only be called while expanding macros.
// Do not call this method directly from within the parser, use nextExprId(int).
return nextExprId(peekPosition());
}

@Override
public long copyExprId(long id) {
return nextExprId(getPosition(id));
}

private List<CelIssue> getIssuesList() {
return issues;
}
Expand Down
48 changes: 48 additions & 0 deletions parser/src/test/java/dev/cel/parser/CelParserImplTest.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow(
assertThat(parseResult.getAst()).isNotNull();
}

@Test
public void parse_nodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build())
.build();
CelValidationResult parseResult = parser.parse("a + b + c");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded");
assertThat(exception.getErrors()).hasSize(1);
}

@Test
public void parse_macroExpansionNodeLimitExceeded_throws() {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");

CelValidationException exception =
assertThrows(CelValidationException.class, parseResult::getAst);
assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded");
assertThat(
exception.getErrors().stream()
.anyMatch(
issue ->
issue
.getMessage()
.contains("could not expand macro: expression node limit exceeded")))
.isTrue();
}

@Test
public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException {
CelParser parser =
CelParserImpl.newBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build())
.build();
CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)");
assertThat(parseResult.hasError()).isFalse();
assertThat(parseResult.getAst()).isNotNull();
}

@Test
@TestParameters("{expression: 'A.map(a?b, c)'}")
@TestParameters("{expression: 'A.all(a?b, c)'}")
Expand Down
Loading