防范 Cypher 注入
什么是 Cypher 注入?
Cypher 注入是一种恶意格式化输入跳出其上下文,并通过更改查询本身来劫持查询并在数据库上执行意外操作的方式。
这与 SQL 注入类似,但影响的是我们的 Cypher 查询语言。
一个展示 SQL 注入的绝佳例子来自 XKCD 漫画,其中描绘了小鲍比·表(Little Bobby Tables)
漫画中描绘了一位母亲,她给孩子取名为 Robert'; DROP TABLE STUDENTS;--
,这样如果他的名字通过字符串拼接(且没有针对注入攻击的保护)被附加到 SQL 语句中,额外的引号将关闭应用程序打开的引号,分号将结束语句,而 --
将注释掉查询的其余部分,从而避免语法错误。
鉴于鲍比的母亲很可能会继续她的“圣战”,她的下一个孩子可能会被命名为针对 Neo4j(领先的图数据库使用的)Cypher。我们称他们为小罗比·标签(Little Robby Labels)。
"Robby' WITH true as ignored MATCH (s:Student) DETACH DELETE s; //"
这是一次 Cypher 注入尝试,与 SQL 注入攻击等效,但改为删除所有 :Student 节点。
如果罗比的学校使用字符串拼接来构建查询,这次攻击可能会成功
String queryString = "CREATE (s:Student) SET s.name = '" + studentName + "'"; Result result = session.run(queryString);
将要运行的查询是
CREATE (s:Student)
SET s.name = 'Robby' WITH true as ignored MATCH (s:Student) DETACH DELETE s; //';
创建 Robby 节点后,所有 :Student 节点都将被找到并删除。
使用参数防范 Cypher 注入
在接收用户输入时,可以使用参数来防止 Cypher 注入。
在上述示例中,不应将学生姓名与 CREATE
查询进行字符串拼接,而应使用参数。
Map<String,Object> params = new HashMap<>(); params.put( "studentName", studentName ); String query = "CREATE (s:Student)" + "\n" + "SET s.name = $studentName"; Result result = transaction.execute( query, params );
参数化查询将如下所示
CREATE (s:Student)
SET s.name = $studentName
当像这样使用参数时,参数不可能修改原始查询,从而阻止任何 Cypher 注入尝试。
这是因为参数与查询是分离的。查询本身被编译成一个可执行计划,一旦编译,就可以使用任何参数映射进行执行。
换句话说,一旦查询计划被编译,它就被确定了,提交给它的数据中的任何内容都无法更改、修改或劫持它。
这种保护是防止字面量 Cypher 注入最简单和最安全的方法。变量和字面量可以作为参数替换,不幸的是,查询中并非所有内容都可以参数化。
同样重要的是要记住,并非所有注入攻击都依赖于字符串字面量的引号转义。例如,以下查询针对数字字面量,在不使用参数化时仍然容易受到攻击
query = "MATCH (user) WHERE user.id =" + userid + ";"
其中恶意 userid 输入是:"1 OR 1 = 1 WITH true AS ignored MATCH (all) DETACH DELETE all; //"
这也可以通过使用参数轻松修复
MATCH (user) WHERE user.id = $userid;
参数与 APOC
APOC 是一个广泛使用的插件,可与 Neo4j 一起安装。可用的过程和函数提供了一些强大的增强功能,在使用 Cypher 时非常有用。在这里使用参数仍然很重要,但值得注意的是,此级别的字符串拼接仍然容易受到 Cypher 注入的影响。
考虑这个查询
CALL apoc.cypher.doIt("CREATE (s:Student) SET s.name = '" + $studentName + "' RETURN true", {})
YIELD value
RETURN value;
即使 studentName
作为参数传递,它现在也将与 CREATE
查询拼接在一起以供执行。这种拼接可能导致被劫持的查询由 APOC 执行。
例如,如果学生姓名是
' MATCH (all) DETACH DELETE all; //
这将由 APOC 作为以下查询执行
CREATE (s:Student) SET s.name = '' MATCH (all) DETACH DELETE all; //' RETURN true
在这种情况下,解决方案是继续将 studentName
作为参数传递给 APOC 过程。
CALL apoc.cypher.doIt("CREATE (s:Student) SET s.name = $name RETURN true", { name: $studentName })
YIELD value
RETURN value;
小罗比·标签再次无能为力!
值得注意的 APOC 过程
apoc.case() apoc.when() apoc.cypher.doIt() apoc.cypher.run() apoc.cypher.runMany() apoc.cypher.runManyReadOnly() apoc.cypher.runSchema() apoc.cypher.runTimeboxed() apoc.cypher.runWrite() apoc.cypher.runFirstColumnMany() apoc.cypher.runFirstColumnSingle() apoc.do.case() apoc.do.when() apoc.export.csv.query() apoc.export.cypher.query() apoc.export.graphml.query() apoc.export.json.query() apoc.graph.fromCypher() apoc.periodic.commit() apoc.periodic.iterate() apoc.periodic.repeat() apoc.periodic.submit() apoc.trigger.add()
所有上述列出的 APOC 过程都包含一种向调用传递参数映射的方式,从而提供了针对注入攻击的保护。
注入攻击类型
在前面的例子中,我们展示了小罗比如何通过删除数据库中的所有数据来毁掉你的一天。但这并不是恶意行为者通过注入利用查询的唯一方式。
信息泄露
另一种可能的注入向量是攻击者使用恶意输入读取他们不应有权访问的信息。
例如;攻击有效载荷为
Robby' OR 1=1 RETURN apoc.text.join(collect(s.name), ','); //
可能会执行为
MATCH (s:Student) WHERE s.name = 'Robby' OR 1=1 RETURN apoc.text.join(collect(s.name), ','); //' RETURN s.name;
以逗号分隔的字符串形式返回数据库中所有学生的姓名。要使此方法成功,客户端应用程序需要易受注入攻击,并且需要将查询结果发送回用户。
盲注
盲注是指攻击者不直接从客户端响应中获取披露的信息,而是可能以不同的方式获取信息。
实现这一点的一种方法是对应用程序的行为做出反应。假设一个网站根据查询的存在结果加载不同的页面。例如,登录页面首先要求输入电子邮件,然后显示“登录以继续”页面或“注册以继续”页面。
query = "MATCH (user) WHERE user.email = '" + email + "' RETURN user IS NOT NULL;"
此查询的结果不会返回给用户,而是应用程序使用用户的存在来显示下一页。通过这种方式,可能的注入可以利用这一点,通过有条件地触发不同的响应。
例如,小罗比想看看他哥哥注册的用户名是什么
"bobby@mail.com' RETURN user.username STARTS WITH 'a';//
如果用户名以 a
开头,查询结果为 true,并显示登录页面。通过这种方式,罗比可以逐个字符地,通过系统地检查每个字符的响应来找出属于他哥哥的用户名。
基于错误的 Cypher 注入
获取信息的另一种方式是恶意行为者利用客户端应用程序返回的错误消息。这可以通过注入错误输入来完成,该输入将输出不同的错误消息,并根据这些消息获取有关数据库的敏感信息。此信息可用于使用下一个有效载荷制作更强大的注入。这可能就像添加一个额外的引号一样简单,以查看服务器是否会返回整个数据库错误。这是一个简单的利用输入的示例
Input: ' RETURN a//
MATCH (s:Student) WHERE s.name = '' RETURN a//' RETURN s;
这会导致以下数据库错误
Variable `a` not defined (line 1, column 44 (offset: 43)) "MATCH (s:Student) WHERE s.name = '' RETURN a//' RETURN s;" ^
如果服务器返回原始错误,则整个查询现在都可见,从而更容易发送更具体的恶意输入。攻击者现在知道至少一个标签的名称以及与其关联的变量。
为了防范这种情况,除了使用参数和清理/验证用户输入外,还应避免向用户返回特定于数据库的错误,并选择更通用的错误。
查询清理
虽然使用字符串拼接构建查询通常不是一个好主意,但并非总是可以避免。节点标签、关系类型和属性名称是 Cypher 中不支持参数化的显著例子。
在这些情况下,清理用户输入很重要。清理是对输入进行修改以确保其有效。在 Cypher 的情况下,这通常意味着转义引号或移除分隔符,这些分隔符会被过早地解释为字符串字面量或标识符的结尾。在接受不受信任的外部输入时,应始终进行清理,有时可能还需要,有关更多信息请参阅二阶注入。
建议在客户端级别进行此清理,然后将其传递给数据库。
Cypher 中的转义字符
转义字符会赋予序列中后续字符不同的含义。在 Cypher 中,字符串字面量和标识符(例如节点标签)的定义可以通过特定字符的开头和结尾来完成,这些字符在正确转义的情况下也可以在表达式内部使用。
在以下部分中,我们将解释如何转义不同 Cypher 类型的分隔符。
Cypher 类型 | 字符类型 | 字符 | 转义序列 |
---|---|---|---|
字符串字面量 |
单引号 |
' |
\' or \u005c' |
Unicode 单引号 |
\u0027 |
\u005c\u0027 or \\u0027 |
|
双引号 |
" |
\" or \u005c" |
|
Unicode 双引号 |
\u0022 |
\u005c\u0022 or \\u0022 |
|
标识符 |
反引号 |
` |
`` |
Unicode 标识符 |
\u0060 |
\u0060\u0060 or `\u0060 |
何时需要清理
节点标签、关系类型和参数可能包含非字母字符,包括数字、符号和空格字符,但必须使用反引号进行转义。例如:node label with spaces
。这意味着在通过字符串拼接动态构建查询时,需要对反引号的转义进行清理。在 Cypher 中,反引号使用两个反引号 ``
进行转义。对于其他类型,例如以单引号 '
或双引号 "
开始和结束的字符串字面量,清理将通过使用反斜杠 \
转义引号字符来完成。请注意,凡是可以使用字符串字面量的地方,也可以使用参数,因此建议始终进行参数化,而不是仅仅清理输入,以避免 Cypher 注入。
这是一个简单的动态标签注入攻击示例
query = "MATCH (s:School)-[:IN]→(c:`" + cityName + "`) RETURN s;
使用此查询,我们希望搜索某个城市中的所有学校,不幸的是,我们的城市名称是节点标签,因此无法对输入进行参数化。
可能的攻击输入将是
Input = `) RETURN 1 as a UNION MATCH (n) RETURN 1 WITH true AS ignored MATCH (n) DETACH DELETE n; //
反引号转义了标签名称的上下文,而括号闭合了节点。这里的 UNION
确保匹配发生,因为如果第一个 MATCH
语句没有返回任何内容,查询的下一部分就不会运行。WITH
将结果集缩小到一行,然后最后一部分将删除数据库中的所有内容。
这种攻击无法通过参数化来避免。要避免这种攻击,必须使用清理。
防范 Cypher 注入的最佳保护是始终将用户输入参数化。如果可能,请更新您的数据模型,以避免需要使用动态标签进行查询。在此示例中,重构将是将城市名称移至参数。
MATCH (s:School)-[:IN]→(c: City { name: $cityName }) RETURN s;
还可以对用户输入添加验证,在这种情况下,在将城市名称传递到数据库之前验证其是否是真实的城市名称,否则拒绝。
此查询所需的清理是转义额外的反引号字符。
SanitizedInput = ``) RETURN 1 as a UNION MATCH (n) RETURN 1 WITH true AS ignored MATCH (n) DETACH DELETE n; //
现在添加的额外反引号确保整个字符串用作节点标签,并且无法跳出该上下文。
反引号的 Unicode 字符;\u0060
也将解析为反引号,并且需要进行清理。在处理用户输入时,考虑客户端所用的编程语言非常重要。例如,输入:\u005C\u00750060
在传递到数据库之前可能会被解析为 \u0060
(\u005C
是反斜杠 \
,\u0075
是 u
),然后它将被数据库解析为反引号!
编写自己的清理函数可能很棘手。这就是为什么强烈建议避免字符串拼接,并以不需要用户输入即可根据节点标签、关系类型和参数动态查询的方式设计数据库。
验证和清理常见漏洞
清理也可以作为清理用户输入的一种技术。另一种保持输入安全和干净的方法是使用验证。验证会检查输入并确保其符合一组特定标准,如果不符合则会拒绝输入,而清理仅清理输入。验证可以与清理一起使用。请记住,这两种技术都伴随着风险。
空白字符检查
检查用户输入中的空白字符听起来是避免注入的好方法,在某些情况下它会奏效,考虑这个例子
"Robby' MATCH (s:Student) DETACH DELETE s; //"
空白字符的验证检查会将此查询标记为无效,但仅检查空白字符是不够的。在 Cypher 中,使用块注释替换空白字符也是有效的,因此以下查询将通过空白字符验证检查
"Robby'/**/MATCH/**/(s:Student)/**/DETACH/**/DELETE/**/s;/**///"
请注意,在这种情况下,过滤 /**/
仍然不够,因为块注释本身可以包含随机可忽略的字符:/**thisisacomment**/
。
检查和清理空白字符可能对您的应用程序有用,但不应将其作为避免 Cypher 注入的安全方法。
Unicode 编码
输入验证和清理的另一个常见漏洞是 Unicode 编码。Unicode 编码是将字符编码为其 Unicode 等效项。例如;单引号字符 '
可以编码为 \u0027
。在清理字符串以移除转义引号字符时,也必须检查其 Unicode 等效项。以下查询乍一看似乎没有转义字符串
"Robby\u0027 MATCH (s:Student) DETACH DELETE s; //"
但实际上,Cypher 会将 Unicode 解析为单引号,并在查询编译时将其视为单引号。
在验证用户名等输入时,通常会检查是否存在保留关键字,例如 admin。Unicode 编码可以作为另一种常见的绕过方法。例如,用户输入 \u0061\u0064\u006d\u0069\u006e
是 admin
的 Unicode。
CREATE (n {username: '\u0061\u0064\u006d\u0069\u006e'}) RETURN n.username
n.username |
---|
"Admin" |
二阶注入
二阶注入发生在输入首次使用时成功过滤和清理,然后存储在数据库中。当应用程序再次使用该值时,恶意代码被执行。
例如;小罗比·标签设置了一个账户,其用户名设置为
LilRob' OR 1=1 WITH true AS hacked MATCH (all) DETACH DELETE all; //
由于用户名直接从用户接收,我们的应用程序使用参数来设置它。
Map<String,Object> params = new HashMap<>(); params.put( "username", username ); String query = "CREATE (u:User)" + "\n" + "SET u.username = $username"; Result result = transaction.execute( query, params );
参数化查询将如下所示
CREATE (u:User) SET u.username = $username;
现在账户已创建,小罗比·表登录并进入设置以更改其用户名。数据库检索其当前用户名,并使用客户端字符串拼接来构建查询以更新它。
query = "MATCH (u:User) WHERE u.username = '" + username + "' SET u.username = $newUsername;"
此查询执行为
MATCH (u:User) WHERE u.username = 'LilRob' OR 1=1 WITH true AS hacked MATCH (all) DETACH DELETE all; //' SET u.username = $newUsername;
恶意代码现在运行,所有用户都被删除了!这就是为什么即使输入似乎不是直接来自用户,也应继续使用清理的原因。
导入数据
并非所有输入都可以作为参数提交。也许一些恶意输入进入了 CSV 文件进行处理。例如,一份包含当年新学生姓名的 CSV 文件。
LOAD CSV WITH HEADERS FROM "file:///students_2021.csv" AS row
CREATE (s:Student)
SET s.year = 2021, s.name = row.student_name
这会受到小罗比·标签的攻击吗?
不,不会。即使无法将参数用于行数据,Cypher 注入在这里仍然不可能。
LOAD 查询独立于要处理的 CSV 文件。这意味着,无论每行内容如何,内容都无法影响或劫持查询本身。
此页面有帮助吗?