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: 4 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Deprecated Groovy-based `LifeCycleHook` and `TraversalSource` creation via init scripts in favor of YAML configuration.
* Updated all default Gremlin Server configs to remove Groovy dependency from initialization.
* Added script engine allowlist to Gremlin Server - the `scriptEngines` YAML configuration now restricts which engines can serve requests; `gremlin-lang` is always available.
* Modified request parameters from `Map<String, Object>` to gremlin-lang compatible `String`.
* Modified HTTP API to expect gremlin-lang strings for parameters and update all GLVs to send requests in new format.
* Added string parameter parsing to `GremlinServer` to prevent traversal injection and excessive nesting depths.
* Modified all GLVs to detect unsupported types in `GremlinLang` and throw consistent error for that case.

[[release-4-0-0-beta-2]]
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)
Expand Down
4 changes: 2 additions & 2 deletions docs/src/dev/provider/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,7 @@ Gremlin-Hints: <hints>
{
"gremlin": string,
"timeoutMs": number,
"bindings": object,
"bindings": string,
"g": string,
"language" : string,
"materializeProperties": string,
Expand Down Expand Up @@ -1064,7 +1064,7 @@ the serializer specified by the `Content-Type` header. The following are the key
|Key |Description |Value |Required
|gremlin |The Gremlin query to execute. |String containing script |Yes
|timeoutMs |The maximum time a query is allowed to execute in milliseconds. |Number between 0 and 2^31-1 |No
|bindings |A map used during query execution. Its usage depends on "language". For "gremlin-groovy", these are the variable bindings. For "gremlin-lang", these are the parameter bindings. |Object (Map) |No
|bindings |A gremlin-lang string that contains a map used during query execution. Its usage depends on "language". For "gremlin-groovy", these are the variable bindings. For "gremlin-lang", these are the parameter bindings. |Object (Map) |No
|g |The name of the graph traversal source to which the query applies. Default: "g" |String containing traversal source name |No
|language |The name of the ScriptEngine to use to parse the gremlin query. Default: "gremlin-lang" |String containing ScriptEngine name |No
|materializeProperties |Whether to include all properties for results. One of "tokens" or "all". |String |No
Expand Down
25 changes: 25 additions & 0 deletions docs/src/upgrade/release-4.x.x.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,31 @@ scriptEngines: {
See: link:https://issues.apache.org/jira/browse/TINKERPOP-2720[TINKERPOP-2720],
link:https://issues.apache.org/jira/browse/TINKERPOP-3107[TINKERPOP-3107]

==== `gremlin-lang` based Parameters

Bindings/parameters that are sent as part of the request are now `gremlin-lang` string maps rather than an actual Map
that would have been serialized based on the serializer used for the request. A side effect of this is that the map key
must be a valid Java identifier. This means that certain items can no longer be sent like `#jsr223` control flags that
were used for the `gremlin-groovy` ScriptEngine. Ensure that you are using valid Java identifiers (e.g. start with a
letter, no spaces, can't use reserved keywords, etc.) for keys.

Since parameters are now text-based gremlin-lang literals, `Traversal` objects can no longer be passed as binding
values. In previous versions it was possible to pass a traversal as a parameter:

[source,text]
----
// 3.x — no longer supported in 4.0
client.submit("g.V(x)", Map.of("x", __.has("name","marko")));
----

This pattern is now rejected by the server as a security measure to prevent traversal injection through parameter maps.
Traversals should be composed directly in the Gremlin query string rather than supplied as parameters.

Parameter maps are also subject to a maximum nesting depth of 32 levels. Deeply nested map or collection structures
beyond this limit will be rejected with an error.

See: link:https://issues.apache.org/jira/browse/TINKERPOP-3247[TINKERPOP-3247]

=== Upgrading for Providers

==== Graph System Providers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public Object[] parseObjectList(final GremlinParser.GenericCollectionLiteralCont
return collectionLiteral.genericLiteral()
.stream()
.filter(Objects::nonNull)
.map(antlr.genericVisitor::visitGenericLiteral)
.map(this::visitGenericLiteral)
.toArray(Object[]::new);
}

Expand All @@ -139,7 +139,7 @@ public Object[] parseObjectVarargs(final GremlinParser.GenericLiteralVarargsCont
return varargsContext.genericLiteralExpr().genericLiteral()
.stream()
.filter(Objects::nonNull)
.map(antlr.genericVisitor::visitGenericLiteral)
.map(this::visitGenericLiteral)
.toArray(Object[]::new);
}

Expand All @@ -153,7 +153,7 @@ public String[] parseStringVarargs(final GremlinParser.StringNullableLiteralVara
return varargsContext.stringNullableLiteral()
.stream()
.filter(Objects::nonNull)
.map(antlr.genericVisitor::parseString)
.map(this::parseString)
.toArray(String[]::new);
}

Expand Down Expand Up @@ -283,12 +283,12 @@ public Object visitGenericLiteralExpr(final GremlinParser.GenericLiteralExprCont
return new Object[0];
case 1:
// handle single generic literal
return antlr.genericVisitor.visitGenericLiteral(ctx.genericLiteral(0));
return this.visitGenericLiteral(ctx.genericLiteral(0));
default:
// handle multiple generic literal separated by comma
final List<Object> genericLiterals = new ArrayList<>();
for (GremlinParser.GenericLiteralContext ic : ctx.genericLiteral()) {
genericLiterals.add(antlr.genericVisitor.visitGenericLiteral(ic));
genericLiterals.add(this.visitGenericLiteral(ic));
}
return genericLiterals.toArray();
}
Expand All @@ -298,7 +298,7 @@ public Object visitGenericLiteralExpr(final GremlinParser.GenericLiteralExprCont
public Object visitGenericSetLiteral(final GremlinParser.GenericSetLiteralContext ctx) {
final Set<Object> result = new HashSet<>(ctx.getChildCount() / 2);
for (GremlinParser.GenericLiteralContext ic : ctx.genericLiteral()) {
result.add(antlr.genericVisitor.visitGenericLiteral(ic));
result.add(this.visitGenericLiteral(ic));
}
return result;
}
Expand Down Expand Up @@ -349,7 +349,8 @@ public Object visitGenericMapLiteral(final GremlinParser.GenericMapLiteralContex
} else if (kctx instanceof GremlinParser.GenericMapLiteralContext) {
key = visitGenericMapLiteral((GremlinParser.GenericMapLiteralContext) kctx);
} else if (kctx instanceof GremlinParser.KeywordContext) {
key = ((GremlinParser.KeywordContext) kctx).getText();
final String keywordText = ((GremlinParser.KeywordContext) kctx).getText();
key = keywordText.equals("null") ? null : keywordText;
} else if (kctx instanceof GremlinParser.NakedKeyContext) {
key = ((GremlinParser.NakedKeyContext) kctx).getText();
} else if (kctx instanceof TerminalNode) {
Expand Down Expand Up @@ -508,7 +509,7 @@ public Object visitBooleanLiteral(final GremlinParser.BooleanLiteralContext ctx)
public Object visitDateLiteral(final GremlinParser.DateLiteralContext ctx) {
if (ctx.stringLiteral() == null)
return DatetimeHelper.datetime();
return DatetimeHelper.parse((String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral()));
return DatetimeHelper.parse((String) this.visitStringLiteral(ctx.stringLiteral()));
}

/**
Expand All @@ -518,7 +519,7 @@ public Object visitDateLiteral(final GremlinParser.DateLiteralContext ctx) {
public Object visitUuidLiteral(final GremlinParser.UuidLiteralContext ctx) {
if (ctx.stringLiteral() == null)
return UUID.randomUUID();
return UUID.fromString((String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral()));
return UUID.fromString((String) this.visitStringLiteral(ctx.stringLiteral()));
}

/**
Expand All @@ -541,8 +542,8 @@ public Object visitCharacterLiteral(final GremlinParser.CharacterLiteralContext
*/
@Override
public Object visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
final Number secondsNum = (Number) antlr.genericVisitor.visitIntegerLiteral(ctx.integerLiteral(0));
final Number nanosNum = (Number) antlr.genericVisitor.visitIntegerLiteral(ctx.integerLiteral(1));
final Number secondsNum = (Number) this.visitIntegerLiteral(ctx.integerLiteral(0));
final Number nanosNum = (Number) this.visitIntegerLiteral(ctx.integerLiteral(1));

final long seconds = secondsNum.longValue();
final long nanos = nanosNum.longValue();
Expand All @@ -568,7 +569,7 @@ public Object visitDurationLiteral(final GremlinParser.DurationLiteralContext ct
*/
@Override
public Object visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
final String base64 = (String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
final String base64 = (String) this.visitStringLiteral(ctx.stringLiteral());
try {
return ByteBuffer.wrap(Base64.getDecoder().decode(base64));
} catch (IllegalArgumentException e) {
Expand Down Expand Up @@ -732,7 +733,7 @@ public Object visitInfLiteral(final GremlinParser.InfLiteralContext ctx) {
public Object visitGenericCollectionLiteral(final GremlinParser.GenericCollectionLiteralContext ctx) {
final List<Object> result = new ArrayList<>(ctx.getChildCount() / 2);
for (GremlinParser.GenericLiteralContext ic : ctx.genericLiteral()) {
result.add(antlr.genericVisitor.visitGenericLiteral(ic));
result.add(this.visitGenericLiteral(ic));
}
return result;
}
Expand All @@ -741,7 +742,7 @@ public Object visitGenericCollectionLiteral(final GremlinParser.GenericCollectio
public Object visitStringNullableLiteral(final GremlinParser.StringNullableLiteralContext ctx) {
if (ctx.K_NULL() != null)
return null;
return antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
return this.visitStringLiteral(ctx.stringLiteral());
}

@Override
Expand All @@ -754,7 +755,7 @@ public Object[] visitStringNullableLiteralVarargs(final GremlinParser.StringNull
.filter(Objects::nonNull)
.filter(p -> p instanceof GremlinParser.StringNullableLiteralContext)
.map(p -> (GremlinParser.StringNullableLiteralContext) p)
.map(antlr.genericVisitor::visitStringNullableLiteral)
.map(this::visitStringNullableLiteral)
.toArray(Object[]::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
*/
package org.apache.tinkerpop.gremlin.language.grammar;

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Map;
import javax.lang.model.SourceVersion;

/**
* Parses Gremlin strings to an {@code Object}, typically to a {@link Traversal}.
*/
Expand All @@ -44,21 +47,15 @@ public static Object parse(final String query) {
* Parse Gremlin string using a specified {@link GremlinAntlrToJava} object.
*/
public static Object parse(final String query, final GremlinVisitor<Object> visitor) {
final CharStream in = CharStreams.fromString(query);
final GremlinLexer lexer = new GremlinLexer(in);
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);

final GremlinLexer lexer = createLexer(query);
final CommonTokenStream tokens = new CommonTokenStream(lexer);

// Setup error handler on parser
final GremlinParser parser = new GremlinParser(tokens);
final GremlinParser parser = createParser(tokens);
// SLL prediction mode is faster than the LL prediction mode when parsing the grammar,
// but it does not cover parsing all types of input. We use the SLL by default, and fallback
// to LL mode if fails to parse the query.
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
parser.removeErrorListeners();
parser.addErrorListener(errorListener);

GremlinParser.QueryListContext queryContext;
try {
Expand Down Expand Up @@ -91,4 +88,95 @@ public static Object parse(final String query, final GremlinVisitor<Object> visi
throw new GremlinParserException("Failed to interpret Gremlin query: " + ex.getMessage(), ex);
}
}

/**
* Parses a gremlin-lang map literal string into a {@code Map<String, Object>} for use as parameters.
* <p>
* Uses {@link ParameterMapVisitor} to prevent traversal injection and validates that all keys are strings
* and no values contain traversals.
*
* @param parameterMapString the gremlin-lang map literal string (e.g. {@code [x:1,y:"marko"]}) or {@code null}/empty
* @return the parsed and validated parameter map
* @throws GremlinParserException if parsing fails or validation detects invalid content
*/
public static Map<String, Object> parseParameters(final String parameterMapString) {
if (parameterMapString == null || parameterMapString.isEmpty()) {
return Map.of();
}

final GremlinParser parser = createParser(parameterMapString);
final GremlinParser.GenericMapLiteralContext mapCtx = parser.genericMapLiteral();

final ParameterMapVisitor visitor = new ParameterMapVisitor(new GremlinAntlrToJava());
final Map<Object, Object> rawMap = (Map<Object, Object>) visitor.visitGenericMapLiteral(mapCtx);
Comment thread
Cole-Greer marked this conversation as resolved.

if (rawMap == null) {
return Map.of();
}

for (final Map.Entry<?, ?> entry : rawMap.entrySet()) {
if (!(entry.getKey() instanceof String)) {
throw new GremlinParserException(
String.format("Parameter map keys must be String, found: %s",
entry.getKey() == null ? "null" : entry.getKey().getClass().getSimpleName()));
}
final String key = (String) entry.getKey();
if (!SourceVersion.isIdentifier(key)) {
throw new GremlinParserException(
String.format("Parameter map key must be a valid identifier: %s", key));
}
validateParameterValue(entry.getValue());
}

return (Map<String, Object>) (Map<?, ?>) rawMap;
}

/**
* Recursively validates that a parameter value does not contain a {@link Traversal}. Nested validation is needed
* because steps like mergeV iterate map values, so a Traversal hiding inside a nested map or collection would still
* be dangerous.
*/
private static void validateParameterValue(final Object value) {
if (value instanceof Traversal) {
throw new GremlinParserException("Traversals are not allowed as parameter values");
Comment thread
kenhuuu marked this conversation as resolved.
}
if (value instanceof Map) {
Comment thread
Cole-Greer marked this conversation as resolved.
for (final Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet()) {
validateParameterValue(e.getKey());
validateParameterValue(e.getValue());
}
}
if (value instanceof Collection) {
for (final Object v : (Collection<?>) value) {
validateParameterValue(v);
}
}
}

/**
* Creates a {@link GremlinParser} from the given input string.
*/
private static GremlinParser createParser(final String input) {
return createParser(new CommonTokenStream(createLexer(input)));
}

/**
* Creates a {@link GremlinParser} from the given {@link GremlinLexer}.
*/
private static GremlinParser createParser(final CommonTokenStream tokens) {
final GremlinParser parser = new GremlinParser(tokens);
parser.removeErrorListeners();
parser.addErrorListener(errorListener);
return parser;
}

/**
* Creates a {@link GremlinLexer} from the given input string with error listeners configured.
*/
private static GremlinLexer createLexer(final String input) {
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(input));
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);
return lexer;
}
}
Loading
Loading