当用户在文本框中输入数据时,会观察到SQL注入的许多可能性.为了防止这种情况,许多方法可用于在SQL查询中使用占位符,这些方法将在输入的下一步代码中替换.同样,我们如何在C#中防止Gremlin Injection?
示例:以下是在图形数据库中添加节点的示例代码.变量的值: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 query = client.CreateGremlinQuery(graph, sb.ToString());
while (query.HasMoreResults){
foreach (dynamic result in await query.ExecuteNextAsync())
{
Console.WriteLine($"\t {JsonConvert.SerializeObject(result)}");
}}
恶意用户可以编写attributeValue之类的
名称: "人"(不含引号)
id: "mary"); gV().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
1> Florian Hock..:
问题最初是关于Gremlin注入的情况,其中Gremlin遍历以查询脚本的形式发送到服务器(例如,Gremlin Server).我在这个场景的原始答案可以在下面找到(Gremlin Scripts).但是,到目前为止,Gremlin语言变体是执行Gremlin遍历的主要方式,这就是为什么我扩展我的答案,因为它与简单的Gremlin脚本的情况非常不同.
Gremlin语言变体
Gremlin语言变体(GLV)是Gremlin在不同主机语言(如Python,Javascript或C#)中的实现.这意味着不要将遍历作为字符串发送到服务器
client.SubmitAsync("g.V().count");
它可以简单地表示为特定语言的代码,然后使用特殊的终端步骤(如next()
或iterate()
)执行:
g.V().Count().Next();
这构建并执行C#中的遍历(在其他语言中看起来基本相同,只是在pascal情况下不是步骤名称).遍历将转换为Gremlin字节码,这是Gremlin遍历的语言无关表示.然后将此字节序列化为GraphSON,以发送到服务器进行评估:
{
"@type" : "g:Bytecode",
"@value" : {
"step" : [ [ "V" ], [ "count" ] ]
}
}
这个非常简单的遍历已经表明GraphSON包含类型信息,特别是自版本2.0以及版本3.0以来更多,这是自TinkerPop 3.3.0以来的默认版本.
攻击者有两种有趣的GraphSON类型,即已经显示的Bytecode可用于执行Gremlin遍历,例如g.V().drop
操作/删除图中的数据,以及g:Lambda
可用于执行任意代码1的数据:
{
"@type" : "g:Lambda",
"@value" : {
"script" : "{ it.get() }",
"language" : "gremlin-groovy",
"arguments" : 1
}
}
但是,攻击者需要将自己的字节码或lambda作为参数添加到现有遍历的一个步骤中.由于字符串只是在GraphSON中被序列化为字符串,无论它是否包含表示lambda或Bytecode的字符串,因此无法以这种方式将代码注入到具有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(query);
var count = await gremlinClient.SubmitWithSingleResultAsync(
"g.V().count().next()");
Assert.NotEqual(0, count);
}
因为这个测试失败count
是0
这表明小鬼服务器执行的g.V().drop().iterate()
穿越.
脚本参数化
现在官方的TinkerPop文档建议使用脚本参数化,而不是简单地将参数直接包含在查询脚本中,就像我们在前面的示例中所做的那样.虽然它通过性能改进激发了这一建议,但它也有助于防止恶意用户输入的注入.要了解脚本参数化的效果,我们必须查看如何将请求发送到Gremlin服务器(取自提供者文档):
{ "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 Server将仅从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
{
{"nodeId", nodeId}
};
await gremlinClient.SubmitAsync(query, arguments);
var count = await gremlinClient.SubmitWithSingleResultAsync(
"g.V().count().next()");
Assert.NotEqual(0, count);
var existQuery = $"g.V().has('{name}', 'id', nodeId).values('id');";
var nodeIdInDb = await gremlinClient.SubmitWithSingleResultAsync(existQuery,
arguments);
Assert.Equal(nodeId, nodeIdInDb);
}
此测试不仅显示g.V().drop()
未执行的测试(否则count
将再次具有该值0
),但它还在最后三行中演示了注入的Gremlin脚本仅用作id
属性的值.
1这种任意代码执行实际上是提供者特定的.例如,像Amazon Neptune 这样的提供商根本不支持lambdas,也可以限制可以使用SandboxExtension为Gremlin Server执行的代码,例如,通过将已知有问题的方法列入黑名单,使用SimpleSandboxExtension或仅通过白名单FileSandboxExtension的无问题方法.