如何防止 C# 中的 Gremlin 注入?
How to prevent Gremlin injection in C#?
当用户在文本框中输入数据时,观察到许多 SQL 注入的可能性。为了防止这种情况,许多方法都可以在 SQL 查询中使用占位符,这些占位符在下一步代码中被输入替换。同样,我们如何防止 C# 中的 Gremlin 注入?
示例:
以下是在图数据库中添加节点的示例代码。变量的值:name 和 nodeId 通过文本框从用户那里获取。
StringBuilder sb = new StringBuilder();
sb.Append("g.addV('" + name + "').property('id','"+nodeId+"')");
/*The following simply executes the gremlin query stored in sb*/
IDocumentQuery<dynamic> query = client.CreateGremlinQuery<dynamic>(graph, sb.ToString());
while (query.HasMoreResults){
foreach (dynamic result in await query.ExecuteNextAsync())
{
Console.WriteLine($"\t {JsonConvert.SerializeObject(result)}");
}}
恶意用户可能会将 attributeValue 写成
名称: "person"(不带引号)
id: "mary');g.V().drop();g.addV('person').property('id', 'thomas"(不带引号)
这将清除所有现有节点并仅添加一个 ID 为:thomas 的节点
如何防止这种情况发生?
我不想将“;”之类的字符列入黑名单或“)”,因为这是允许作为某些数据的输入。
注意:
Gremlin 是图形数据库中使用的一种遍历语言:
https://tinkerpop.apache.org/gremlin.html
https://docs.microsoft.com/en-us/azure/cosmos-db/gremlin-support
问题最初是关于以查询脚本的形式将 Gremlin 遍历发送到服务器(例如 Gremlin 服务器)的情况下的 Gremlin 注入。我对这种情况的原始答案可以在下面找到 (Gremlin Scripts)。但是,到目前为止,Gremlin Language Variants 是执行 Gremlin 遍历的主要方式,这就是为什么我扩展了对它们的回答,因为它与简单的 Gremlin 脚本的情况非常不同。
Gremlin 语言变体
Gremlin 语言变体 (GLV) 是 Gremlin 在 Python、JavaScript 或 C# 等不同宿主语言中的实现。这意味着不像
那样将遍历作为字符串发送到服务器
client.SubmitAsync<object>("g.V().count");
它可以简单地表示为特定语言的代码,然后用特殊的 terminal step(如 next()
或 iterate()
)执行:
g.V().Count().Next();
这将在 C# 中构建并执行遍历(在其他语言中看起来基本相同,只是步骤名称不是 Pascal 大小写)。遍历将转换为 Gremlin 字节码,它是 Gremlin 遍历的独立于语言的表示。此字节码随后将被序列化为 GraphSON 以发送到服务器进行评估:
{
"@type" : "g:Bytecode",
"@value" : {
"step" : [ [ "V" ], [ "count" ] ]
}
}
这个非常简单的遍历已经表明 GraphSON 包含类型信息,尤其是从 2.0 版开始,在 TinkerPop 3.3.0 之后的默认版本 3.0 中更是如此。
对于攻击者来说,有两种有趣的 GraphSON 类型,即已经展示的字节码,可用于执行 Gremlin 遍历,例如 g.V().drop
以操作/从图中删除数据,以及 g:Lambda
可以用于执行任意代码1:
{
"@type" : "g:Lambda",
"@value" : {
"script" : "{ it.get() }",
"language" : "gremlin-groovy",
"arguments" : 1
}
}
但是,攻击者需要将他自己的字节码或 lambda 作为参数添加到作为现有遍历一部分的步骤。由于字符串在 GraphSON 中将简单地序列化为字符串,无论它是否包含表示 lambda 或字节码的内容,因此不可能以这种方式将代码注入到带有 GLV 的 Gremlin 遍历中。该代码将被简单地视为一个字符串。唯一可行的方法是当攻击者能够直接向步骤提供字节码或 Lambda 对象时,但我想不出任何允许这样做的场景。
因此,据我所知,使用 GLV 时无法将代码注入 Gremlin 遍历。 这与是否使用绑定无关。
以下部分是将遍历作为查询字符串发送到服务器的场景的原始答案:
Gremlin 脚本
您的示例确实会产生您可以称之为 Gremlin 注入 的结果。我用 Gremlin.Net 测试了它,但它应该以与任何 Gremlin 驱动程序相同的方式工作。这是证明注入确实有效的测试:
var gremlinServer = new GremlinServer("localhost");
using (var gremlinClient = new GremlinClient(gremlinServer))
{
var name = "person";
var nodeId = "mary').next();g.V().drop().iterate();g.V().has('id', 'thomas";
var query = "g.addV('" + name + "').property('id','" + nodeId + "')";
await gremlinClient.SubmitAsync<object>(query);
var count = await gremlinClient.SubmitWithSingleResultAsync<long>(
"g.V().count().next()");
Assert.NotEqual(0, count);
}
此测试失败,因为 count
是 0
,这表明 Gremlin 服务器执行了 g.V().drop().iterate()
遍历。
脚本参数化
现在正式的 TinkerPop 文档 recommends to use script parameterization instead of simply including the parameters directly in the query script like we did in the previous example. While it motivates this recommendation with performance improvements, it also helps to prevent injections by malicious user input. To understand the effect of script parameterization here, we have to take a look at how a request is sent to the Gremlin Server (taken from the Provider Documentation):
{ "requestId":"1d6d02bd-8e56-421d-9438-3bd6d0079ff1",
"op":"eval",
"processor":"",
"args":{"gremlin":"g.traversal().V(x).out()",
"bindings":{"x":1},
"language":"gremlin-groovy"}}
正如我们在请求消息的 JSON 表示中看到的那样,Gremlin 脚本的参数作为绑定与脚本本身分开发送。 (参数在这里命名为 x
,值为 1
。)
这里重要的是 Gremlin 服务器将只执行来自 gremlin
元素的脚本,然后将来自 bindings
元素的参数作为原始值包含在内。
一个简单的测试,看看使用绑定可以防止注入:
var gremlinServer = new GremlinServer("localhost");
using (var gremlinClient = new GremlinClient(gremlinServer))
{
var name = "person";
var nodeId = "mary').next();g.V().drop().iterate();g.V().has('id', 'thomas";
var query = "g.addV('" + name + "').property('id', nodeId)";
var arguments = new Dictionary<string, object>
{
{"nodeId", nodeId}
};
await gremlinClient.SubmitAsync<object>(query, arguments);
var count = await gremlinClient.SubmitWithSingleResultAsync<long>(
"g.V().count().next()");
Assert.NotEqual(0, count);
var existQuery = $"g.V().has('{name}', 'id', nodeId).values('id');";
var nodeIdInDb = await gremlinClient.SubmitWithSingleResultAsync<string>(existQuery,
arguments);
Assert.Equal(nodeId, nodeIdInDb);
}
这个测试通过,它不仅表明 g.V().drop()
没有被执行(否则 count
将再次具有值 0
),而且在最后三行中也表明注入的 Gremlin 脚本只是用作 id
属性.
的值
1 这种任意代码执行实际上是特定于提供者的。一些提供商,例如 Amazon Neptune,例如 don't support lambdas at all and it is also possible to restrict the code that can be executed with a SandboxExtension for the Gremlin Server,例如,通过使用 SimpleSandboxExtension 将已知有问题的方法列入黑名单,或者通过使用 FileSandboxExtension[=] 仅将已知没有问题的方法列入白名单87=].
当用户在文本框中输入数据时,观察到许多 SQL 注入的可能性。为了防止这种情况,许多方法都可以在 SQL 查询中使用占位符,这些占位符在下一步代码中被输入替换。同样,我们如何防止 C# 中的 Gremlin 注入?
示例: 以下是在图数据库中添加节点的示例代码。变量的值:name 和 nodeId 通过文本框从用户那里获取。
StringBuilder sb = new StringBuilder();
sb.Append("g.addV('" + name + "').property('id','"+nodeId+"')");
/*The following simply executes the gremlin query stored in sb*/
IDocumentQuery<dynamic> query = client.CreateGremlinQuery<dynamic>(graph, sb.ToString());
while (query.HasMoreResults){
foreach (dynamic result in await query.ExecuteNextAsync())
{
Console.WriteLine($"\t {JsonConvert.SerializeObject(result)}");
}}
恶意用户可能会将 attributeValue 写成
名称: "person"(不带引号)
id: "mary');g.V().drop();g.addV('person').property('id', 'thomas"(不带引号)
这将清除所有现有节点并仅添加一个 ID 为:thomas 的节点
如何防止这种情况发生?
我不想将“;”之类的字符列入黑名单或“)”,因为这是允许作为某些数据的输入。
注意: Gremlin 是图形数据库中使用的一种遍历语言:
https://tinkerpop.apache.org/gremlin.html
https://docs.microsoft.com/en-us/azure/cosmos-db/gremlin-support
问题最初是关于以查询脚本的形式将 Gremlin 遍历发送到服务器(例如 Gremlin 服务器)的情况下的 Gremlin 注入。我对这种情况的原始答案可以在下面找到 (Gremlin Scripts)。但是,到目前为止,Gremlin Language Variants 是执行 Gremlin 遍历的主要方式,这就是为什么我扩展了对它们的回答,因为它与简单的 Gremlin 脚本的情况非常不同。
Gremlin 语言变体
Gremlin 语言变体 (GLV) 是 Gremlin 在 Python、JavaScript 或 C# 等不同宿主语言中的实现。这意味着不像
那样将遍历作为字符串发送到服务器client.SubmitAsync<object>("g.V().count");
它可以简单地表示为特定语言的代码,然后用特殊的 terminal step(如 next()
或 iterate()
)执行:
g.V().Count().Next();
这将在 C# 中构建并执行遍历(在其他语言中看起来基本相同,只是步骤名称不是 Pascal 大小写)。遍历将转换为 Gremlin 字节码,它是 Gremlin 遍历的独立于语言的表示。此字节码随后将被序列化为 GraphSON 以发送到服务器进行评估:
{
"@type" : "g:Bytecode",
"@value" : {
"step" : [ [ "V" ], [ "count" ] ]
}
}
这个非常简单的遍历已经表明 GraphSON 包含类型信息,尤其是从 2.0 版开始,在 TinkerPop 3.3.0 之后的默认版本 3.0 中更是如此。
对于攻击者来说,有两种有趣的 GraphSON 类型,即已经展示的字节码,可用于执行 Gremlin 遍历,例如 g.V().drop
以操作/从图中删除数据,以及 g:Lambda
可以用于执行任意代码1:
{
"@type" : "g:Lambda",
"@value" : {
"script" : "{ it.get() }",
"language" : "gremlin-groovy",
"arguments" : 1
}
}
但是,攻击者需要将他自己的字节码或 lambda 作为参数添加到作为现有遍历一部分的步骤。由于字符串在 GraphSON 中将简单地序列化为字符串,无论它是否包含表示 lambda 或字节码的内容,因此不可能以这种方式将代码注入到带有 GLV 的 Gremlin 遍历中。该代码将被简单地视为一个字符串。唯一可行的方法是当攻击者能够直接向步骤提供字节码或 Lambda 对象时,但我想不出任何允许这样做的场景。
因此,据我所知,使用 GLV 时无法将代码注入 Gremlin 遍历。 这与是否使用绑定无关。
以下部分是将遍历作为查询字符串发送到服务器的场景的原始答案:
Gremlin 脚本
您的示例确实会产生您可以称之为 Gremlin 注入 的结果。我用 Gremlin.Net 测试了它,但它应该以与任何 Gremlin 驱动程序相同的方式工作。这是证明注入确实有效的测试:
var gremlinServer = new GremlinServer("localhost");
using (var gremlinClient = new GremlinClient(gremlinServer))
{
var name = "person";
var nodeId = "mary').next();g.V().drop().iterate();g.V().has('id', 'thomas";
var query = "g.addV('" + name + "').property('id','" + nodeId + "')";
await gremlinClient.SubmitAsync<object>(query);
var count = await gremlinClient.SubmitWithSingleResultAsync<long>(
"g.V().count().next()");
Assert.NotEqual(0, count);
}
此测试失败,因为 count
是 0
,这表明 Gremlin 服务器执行了 g.V().drop().iterate()
遍历。
脚本参数化
现在正式的 TinkerPop 文档 recommends to use script parameterization instead of simply including the parameters directly in the query script like we did in the previous example. While it motivates this recommendation with performance improvements, it also helps to prevent injections by malicious user input. To understand the effect of script parameterization here, we have to take a look at how a request is sent to the Gremlin Server (taken from the Provider Documentation):
{ "requestId":"1d6d02bd-8e56-421d-9438-3bd6d0079ff1",
"op":"eval",
"processor":"",
"args":{"gremlin":"g.traversal().V(x).out()",
"bindings":{"x":1},
"language":"gremlin-groovy"}}
正如我们在请求消息的 JSON 表示中看到的那样,Gremlin 脚本的参数作为绑定与脚本本身分开发送。 (参数在这里命名为 x
,值为 1
。)
这里重要的是 Gremlin 服务器将只执行来自 gremlin
元素的脚本,然后将来自 bindings
元素的参数作为原始值包含在内。
一个简单的测试,看看使用绑定可以防止注入:
var gremlinServer = new GremlinServer("localhost");
using (var gremlinClient = new GremlinClient(gremlinServer))
{
var name = "person";
var nodeId = "mary').next();g.V().drop().iterate();g.V().has('id', 'thomas";
var query = "g.addV('" + name + "').property('id', nodeId)";
var arguments = new Dictionary<string, object>
{
{"nodeId", nodeId}
};
await gremlinClient.SubmitAsync<object>(query, arguments);
var count = await gremlinClient.SubmitWithSingleResultAsync<long>(
"g.V().count().next()");
Assert.NotEqual(0, count);
var existQuery = $"g.V().has('{name}', 'id', nodeId).values('id');";
var nodeIdInDb = await gremlinClient.SubmitWithSingleResultAsync<string>(existQuery,
arguments);
Assert.Equal(nodeId, nodeIdInDb);
}
这个测试通过,它不仅表明 g.V().drop()
没有被执行(否则 count
将再次具有值 0
),而且在最后三行中也表明注入的 Gremlin 脚本只是用作 id
属性.
1 这种任意代码执行实际上是特定于提供者的。一些提供商,例如 Amazon Neptune,例如 don't support lambdas at all and it is also possible to restrict the code that can be executed with a SandboxExtension for the Gremlin Server,例如,通过使用 SimpleSandboxExtension 将已知有问题的方法列入黑名单,或者通过使用 FileSandboxExtension[=] 仅将已知没有问题的方法列入白名单87=].