Skip to content

Commit 6eb4645

Browse files
Support for groovy static analysis for groovy scripts (#14844)
1 parent fb577ed commit 6eb4645

File tree

12 files changed

+692
-38
lines changed

12 files changed

+692
-38
lines changed

pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import org.apache.pinot.core.util.ListenerConfigUtil;
8282
import org.apache.pinot.query.mailbox.MailboxService;
8383
import org.apache.pinot.query.service.dispatch.QueryDispatcher;
84+
import org.apache.pinot.segment.local.function.GroovyFunctionEvaluator;
8485
import org.apache.pinot.spi.accounting.ThreadResourceUsageProvider;
8586
import org.apache.pinot.spi.cursors.ResponseStoreService;
8687
import org.apache.pinot.spi.env.PinotConfiguration;
@@ -478,6 +479,11 @@ public void start()
478479
_participantHelixManager.addPreConnectCallback(
479480
() -> _brokerMetrics.addMeteredGlobalValue(BrokerMeter.HELIX_ZOOKEEPER_RECONNECTS, 1L));
480481

482+
// Initializing Groovy execution security
483+
GroovyFunctionEvaluator.configureGroovySecurity(
484+
_brokerConf.getProperty(CommonConstants.Groovy.GROOVY_QUERY_STATIC_ANALYZER_CONFIG,
485+
_brokerConf.getProperty(CommonConstants.Groovy.GROOVY_ALL_STATIC_ANALYZER_CONFIG)));
486+
481487
// Register the service status handler
482488
registerServiceStatusHandler();
483489

pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import org.apache.pinot.core.transport.ServerInstance;
9191
import org.apache.pinot.core.util.GapfillUtils;
9292
import org.apache.pinot.query.parser.utils.ParserUtils;
93+
import org.apache.pinot.segment.local.function.GroovyFunctionEvaluator;
9394
import org.apache.pinot.spi.auth.AuthorizationResult;
9495
import org.apache.pinot.spi.config.table.FieldConfig;
9596
import org.apache.pinot.spi.config.table.QueryConfig;
@@ -515,9 +516,7 @@ protected BrokerResponse doHandleRequest(long requestId, String query, SqlNodeAn
515516
}
516517

517518
HandlerContext handlerContext = getHandlerContext(offlineTableConfig, realtimeTableConfig);
518-
if (handlerContext._disableGroovy) {
519-
rejectGroovyQuery(serverPinotQuery);
520-
}
519+
validateGroovyScript(serverPinotQuery, handlerContext._disableGroovy);
521520
if (handlerContext._useApproximateFunction) {
522521
handleApproximateFunctionOverride(serverPinotQuery);
523522
}
@@ -1429,45 +1428,59 @@ private static class HandlerContext {
14291428
* Verifies that no groovy is present in the PinotQuery when disabled.
14301429
*/
14311430
@VisibleForTesting
1432-
static void rejectGroovyQuery(PinotQuery pinotQuery) {
1431+
static void validateGroovyScript(PinotQuery pinotQuery, boolean disableGroovy) {
14331432
List<Expression> selectList = pinotQuery.getSelectList();
14341433
for (Expression expression : selectList) {
1435-
rejectGroovyQuery(expression);
1434+
validateGroovyScript(expression, disableGroovy);
14361435
}
14371436
List<Expression> orderByList = pinotQuery.getOrderByList();
14381437
if (orderByList != null) {
14391438
for (Expression expression : orderByList) {
14401439
// NOTE: Order-by is always a Function with the ordering of the Expression
1441-
rejectGroovyQuery(expression.getFunctionCall().getOperands().get(0));
1440+
validateGroovyScript(expression.getFunctionCall().getOperands().get(0), disableGroovy);
14421441
}
14431442
}
14441443
Expression havingExpression = pinotQuery.getHavingExpression();
14451444
if (havingExpression != null) {
1446-
rejectGroovyQuery(havingExpression);
1445+
validateGroovyScript(havingExpression, disableGroovy);
14471446
}
14481447
Expression filterExpression = pinotQuery.getFilterExpression();
14491448
if (filterExpression != null) {
1450-
rejectGroovyQuery(filterExpression);
1449+
validateGroovyScript(filterExpression, disableGroovy);
14511450
}
14521451
List<Expression> groupByList = pinotQuery.getGroupByList();
14531452
if (groupByList != null) {
14541453
for (Expression expression : groupByList) {
1455-
rejectGroovyQuery(expression);
1454+
validateGroovyScript(expression, disableGroovy);
14561455
}
14571456
}
14581457
}
14591458

1460-
private static void rejectGroovyQuery(Expression expression) {
1459+
private static void validateGroovyScript(Expression expression, boolean disableGroovy) {
14611460
Function function = expression.getFunctionCall();
14621461
if (function == null) {
14631462
return;
14641463
}
14651464
if (function.getOperator().equals("groovy")) {
1466-
throw new BadQueryRequestException("Groovy transform functions are disabled for queries");
1465+
if (disableGroovy) {
1466+
throw new BadQueryRequestException("Groovy transform functions are disabled for queries");
1467+
} else {
1468+
groovySecureAnalysis(function);
1469+
}
14671470
}
14681471
for (Expression operandExpression : function.getOperands()) {
1469-
rejectGroovyQuery(operandExpression);
1472+
validateGroovyScript(operandExpression, disableGroovy);
1473+
}
1474+
}
1475+
1476+
private static void groovySecureAnalysis(Function function) {
1477+
List<Expression> operands = function.getOperands();
1478+
if (operands == null || operands.size() < 2) {
1479+
throw new BadQueryRequestException("Groovy transform function must have at least 2 argument");
14701480
}
1481+
// second argument in the groovy function is groovy script
1482+
String script = operands.get(1).getLiteral().getStringValue();
1483+
GroovyFunctionEvaluator.parseGroovyScript(String.format("groovy({%s})", script));
14711484
}
14721485

14731486
/**

pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/QueryValidationTest.java

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@
1919

2020
package org.apache.pinot.broker.requesthandler;
2121

22+
import com.fasterxml.jackson.core.JsonProcessingException;
2223
import com.google.common.collect.ImmutableMap;
2324
import java.util.Map;
2425
import org.apache.pinot.common.request.PinotQuery;
26+
import org.apache.pinot.segment.local.function.GroovyFunctionEvaluator;
27+
import org.apache.pinot.segment.local.function.GroovyStaticAnalyzerConfig;
2528
import org.apache.pinot.sql.parsers.CalciteSqlParser;
2629
import org.testng.Assert;
2730
import org.testng.annotations.Test;
2831

32+
import static org.testng.Assert.assertTrue;
33+
import static org.testng.Assert.fail;
34+
2935

3036
public class QueryValidationTest {
3137

@@ -93,24 +99,64 @@ public void testNonExistingColumns() {
9399
}
94100

95101
@Test
96-
public void testRejectGroovyQuery() {
97-
testRejectGroovyQuery(
102+
public void testValidateGroovyQuery() {
103+
testValidateGroovyQuery(
98104
"SELECT groovy('{\"returnType\":\"INT\",\"isSingleValue\":true}', 'arg0 + arg1', colA, colB) FROM foo", true);
99-
testRejectGroovyQuery(
105+
testValidateGroovyQuery(
100106
"SELECT GROOVY('{\"returnType\":\"INT\",\"isSingleValue\":true}', 'arg0 + arg1', colA, colB) FROM foo", true);
101-
testRejectGroovyQuery(
107+
testValidateGroovyQuery(
102108
"SELECT groo_vy('{\"returnType\":\"INT\",\"isSingleValue\":true}', 'arg0 + arg1', colA, colB) FROM foo", true);
103-
testRejectGroovyQuery(
109+
testValidateGroovyQuery(
104110
"SELECT foo FROM bar WHERE GROOVY('{\"returnType\":\"STRING\",\"isSingleValue\":true}', 'arg0 + arg1', colA,"
105111
+ " colB) = 'foobarval'", true);
106-
testRejectGroovyQuery(
112+
testValidateGroovyQuery(
107113
"SELECT COUNT(colA) FROM bar GROUP BY GROOVY('{\"returnType\":\"STRING\",\"isSingleValue\":true}', "
108114
+ "'arg0 + arg1', colA, colB)", true);
109-
testRejectGroovyQuery(
115+
testValidateGroovyQuery(
110116
"SELECT foo FROM bar HAVING GROOVY('{\"returnType\":\"STRING\",\"isSingleValue\":true}', 'arg0 + arg1', colA,"
111117
+ " colB) = 'foobarval'", true);
112118

113-
testRejectGroovyQuery("SELECT foo FROM bar", false);
119+
testValidateGroovyQuery("SELECT foo FROM bar", false);
120+
}
121+
122+
@Test
123+
public void testGroovyScripts()
124+
throws JsonProcessingException {
125+
// setup secure groovy config
126+
GroovyFunctionEvaluator.setGroovyStaticAnalyzerConfig(GroovyStaticAnalyzerConfig.createDefault());
127+
128+
String inValidGroovyQuery = "SELECT groovy('{\"returnType\":\"INT\",\"isSingleValue\":true}') FROM foo";
129+
runUnsupportedGroovy(inValidGroovyQuery, "Groovy transform function must have at least 2 argument");
130+
131+
String groovyInvalidMethodInvokeQuery =
132+
"SELECT groovy('{\"returnType\":\"STRING\",\"isSingleValue\":true}', 'return [\"bash\", \"-c\", \"echo Hello,"
133+
+ " World!\"].execute().text') FROM foo";
134+
runUnsupportedGroovy(groovyInvalidMethodInvokeQuery, "Expression [MethodCallExpression] is not allowed");
135+
136+
String groovyInvalidImportsQuery =
137+
"SELECT groovy( '{\"returnType\":\"INT\",\"isSingleValue\":true}', 'def args = [\"QuickStart\", \"-type\", "
138+
+ "\"REALTIME\"] as String[]; org.apache.pinot.tools.admin.PinotAdministrator.main(args); 2') FROM foo";
139+
runUnsupportedGroovy(groovyInvalidImportsQuery, "Indirect import checks prevents usage of expression");
140+
141+
String groovyInOrderByClause =
142+
"SELECT colA, colB FROM foo ORDER BY groovy('{\"returnType\":\"STRING\",\"isSingleValue\":true}', 'return "
143+
+ "[\"bash\", \"-c\", \"echo Hello, World!\"].execute().text') DESC";
144+
runUnsupportedGroovy(groovyInOrderByClause, "Expression [MethodCallExpression] is not allowed");
145+
146+
String groovyInHavingClause =
147+
"SELECT colA, SUM(colB) AS totalB, groovy('{\"returnType\":\"DOUBLE\",\"isSingleValue\":true}', 'arg0 / "
148+
+ "arg1', SUM(colB), COUNT(*)) AS avgB FROM foo GROUP BY colA HAVING groovy('{\"returnType\":\"BOOLEAN\","
149+
+ "\"isSingleValue\":true}', 'System.metaClass.methods.each { method -> if (method.name.md5() == "
150+
+ "\"f24f62eeb789199b9b2e467df3b1876b\") {method.invoke(System, 10)} }', SUM(colB))";
151+
runUnsupportedGroovy(groovyInHavingClause, "Indirect import checks prevents usage of expression");
152+
153+
String groovyInWhereClause =
154+
"SELECT colA, colB FROM foo WHERE groovy('{\"returnType\":\"BOOLEAN\",\"isSingleValue\":true}', 'System.exit"
155+
+ "(10)', colA)";
156+
runUnsupportedGroovy(groovyInWhereClause, "Indirect import checks prevents usage of expression");
157+
158+
// Reset groovy config for rest of the testing
159+
GroovyFunctionEvaluator.setGroovyStaticAnalyzerConfig(null);
114160
}
115161

116162
@Test
@@ -121,24 +167,34 @@ public void testReplicaGroupToQueryInvalidQuery() {
121167
() -> BaseSingleStageBrokerRequestHandler.validateRequest(pinotQuery, 10));
122168
}
123169

124-
private void testRejectGroovyQuery(String query, boolean queryContainsGroovy) {
170+
private void testValidateGroovyQuery(String query, boolean queryContainsGroovy) {
125171
PinotQuery pinotQuery = CalciteSqlParser.compileToPinotQuery(query);
126172

127173
try {
128-
BaseSingleStageBrokerRequestHandler.rejectGroovyQuery(pinotQuery);
174+
BaseSingleStageBrokerRequestHandler.validateGroovyScript(pinotQuery, queryContainsGroovy);
129175
if (queryContainsGroovy) {
130-
Assert.fail("Query should have failed since groovy was found in query: " + pinotQuery);
176+
fail("Query should have failed since groovy was found in query: " + pinotQuery);
131177
}
132178
} catch (Exception e) {
133179
Assert.assertEquals(e.getMessage(), "Groovy transform functions are disabled for queries");
134180
}
135181
}
136182

183+
private static void runUnsupportedGroovy(String query, String errorMsg) {
184+
try {
185+
PinotQuery pinotQuery = CalciteSqlParser.compileToPinotQuery(query);
186+
BaseSingleStageBrokerRequestHandler.validateGroovyScript(pinotQuery, false);
187+
fail("Query should have failed since malicious groovy was found in query");
188+
} catch (Exception e) {
189+
assertTrue(e.getMessage().contains(errorMsg));
190+
}
191+
}
192+
137193
private void testUnsupportedQuery(String query, String errorMessage) {
138194
try {
139195
PinotQuery pinotQuery = CalciteSqlParser.compileToPinotQuery(query);
140196
BaseSingleStageBrokerRequestHandler.validateRequest(pinotQuery, 1000);
141-
Assert.fail("Query should have failed");
197+
fail("Query should have failed");
142198
} catch (Exception e) {
143199
Assert.assertEquals(e.getMessage(), errorMessage);
144200
}
@@ -149,7 +205,7 @@ private void testNonExistingColumns(String rawTableName, boolean isCaseInsensiti
149205
try {
150206
PinotQuery pinotQuery = CalciteSqlParser.compileToPinotQuery(query);
151207
BaseSingleStageBrokerRequestHandler.updateColumnNames(rawTableName, pinotQuery, isCaseInsensitive, columnNameMap);
152-
Assert.fail("Query should have failed");
208+
fail("Query should have failed");
153209
} catch (Exception e) {
154210
Assert.assertEquals(errorMessage, e.getMessage());
155211
}
@@ -161,7 +217,7 @@ private void testExistingColumns(String rawTableName, boolean isCaseInsensitive,
161217
PinotQuery pinotQuery = CalciteSqlParser.compileToPinotQuery(query);
162218
BaseSingleStageBrokerRequestHandler.updateColumnNames(rawTableName, pinotQuery, isCaseInsensitive, columnNameMap);
163219
} catch (Exception e) {
164-
Assert.fail("Query should have succeeded");
220+
fail("Query should have succeeded");
165221
}
166222
}
167223
}

pinot-common/src/main/java/org/apache/pinot/common/metrics/ControllerMeter.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ public enum ControllerMeter implements AbstractMetrics.Meter {
7070
IDEAL_STATE_UPDATE_RETRY("IdealStateUpdateRetry", false),
7171
IDEAL_STATE_UPDATE_SUCCESS("IdealStateUpdateSuccess", false);
7272

73-
7473
private final String _brokerMeterName;
7574
private final String _unit;
7675
private final boolean _global;

pinot-controller/src/main/java/org/apache/pinot/controller/BaseControllerStarter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
import org.apache.pinot.core.segment.processing.lifecycle.PinotSegmentLifecycleEventListenerManager;
126126
import org.apache.pinot.core.transport.ListenerConfig;
127127
import org.apache.pinot.core.util.ListenerConfigUtil;
128+
import org.apache.pinot.segment.local.function.GroovyFunctionEvaluator;
128129
import org.apache.pinot.segment.local.utils.TableConfigUtils;
129130
import org.apache.pinot.spi.config.table.TableConfig;
130131
import org.apache.pinot.spi.crypt.PinotCrypterFactory;
@@ -387,7 +388,8 @@ public ControllerConf getConfig() {
387388
}
388389

389390
@Override
390-
public void start() {
391+
public void start()
392+
throws Exception {
391393
LOGGER.info("Starting Pinot controller in mode: {}. (Version: {})", _controllerMode.name(), PinotVersion.VERSION);
392394
LOGGER.info("Controller configs: {}", new PinotAppConfigs(getConfig()).toJSONString());
393395
long startTimeMs = System.currentTimeMillis();
@@ -412,6 +414,11 @@ public void start() {
412414
break;
413415
}
414416

417+
// Initializing Groovy execution security
418+
GroovyFunctionEvaluator.configureGroovySecurity(
419+
_config.getProperty(CommonConstants.Groovy.GROOVY_INGESTION_STATIC_ANALYZER_CONFIG,
420+
_config.getProperty(CommonConstants.Groovy.GROOVY_ALL_STATIC_ANALYZER_CONFIG)));
421+
415422
ServiceStatus.setServiceStatusCallback(_helixParticipantInstanceId,
416423
new ServiceStatus.MultipleCallbackServiceStatusCallback(_serviceStatusCallbackList));
417424
_controllerMetrics.addTimedValue(ControllerTimer.STARTUP_SUCCESS_DURATION_MS,

0 commit comments

Comments
 (0)