为 Web 开发增加安全性的简单方法
作为一名软件开发人员,我亲眼目睹过开发人员 rush 完成分配给他们的功能,几乎或根本不考虑代码中的安全性——没有安全指南,没有编码标准,只是一味地冲刺以完成功能。接下来是安全审查,软件显然未通过,然后进入安全加固阶段。
虽然尝试提高代码的安全性显然是一件好事,但通常进行安全加固的时间是在代码开发的最后阶段,并且与软件开发的基本性质一样,更改代码几乎总是会导致软件偏离成熟状态。因此,几乎已结束开发阶段的软件在安全加固阶段再次被推向不稳定状态。这真的有必要吗?
为什么开发人员不能从一开始就使代码安全呢?可以做些什么来提高开发人员对应用程序安全策略的认识,以便他们在开发下一个应用程序时更加知情和警惕?在本文中,我将讨论开发人员如何有效地做到这一点。
一种简单的方法是改变开发人员的编码风格,使他们编写本质上安全的代码。此外,遵循与应用程序安全性相关的简单策略可以产生很大的不同。这有时不是一件容易的事情,但如果遵循的实践简单易行,那就不会非常困难。
让我们看看软件中常见的一些安全问题/缺陷,以及可以应用哪些相应的安全机制和策略来应对它们。这些机制通常可以在所有编程语言中实现,并遵循 OWASP 代码开发指南。但是,为了开源文化,我在本文的示例中使用 PHP 作为语言。
SQL 注入让我们从最著名的漏洞开始。它也是使用最广泛且最容易对 Web 发起攻击的漏洞之一。然而,许多人不知道的是,它也很容易预防。让我们首先考虑一下什么是 SQL 注入攻击。
假设您的应用程序中有一个用于用户名字段的文本框。当用户填写它时,您将数据带到后端并触发对数据库的查询——类似于这样
<Input Type = "Text" value ="username" name = "username">
<?php $username = $_POST['username']; ?>
然后,SQL 查询
SELECT * FROM table WHERE name = '" + $username + '"
攻击此系统的一种简单方法是在文本框中键入 "'" 或 "'1'='1"。现在生成的数据库查询将是
SELECT * FROM table WHERE name = ' ' or '1'='1'
正如您所看到的,此条件始终为真,并且在执行时,查询只会拆分表中的所有行。这是一个简单的示例,但在现实生活中,此类攻击非常严重,可能会在几秒钟内使整个应用程序瘫痪,因为它们直接针对数据库。
那么,如何预防这种情况呢?简单的逻辑是,不应直接传递从前端获取的输入,而应彻底检查,然后才将其作为查询的一部分发送到数据库。以下是最常见和有效的方法
参数化查询: 这种查询产生的结果与普通 SQL 查询完全相同,但不同之处在于,您需要先定义 SQL 代码,然后再将参数传递给查询。因此,即使有人试图通过将恶意数据传递给查询来发起攻击,查询也会搜索与作为输入发送的任何内容完全匹配的内容。例如,如果有人尝试传递 ' 或 '1=1 作为数据,查询将在数据库中查找数据的字面匹配项。
以下是如何在 PHP 中编写参数化查询的示例(有关参数化查询的更多信息,请参阅您的编程语言手册)
/* Prepared statement, stage 1: prepare */
if (!($stmt = $mysqli->prepare("INSERT INTO test(id) VALUES (?)"))) {
echo "Prepare failed: (" . $mysqli->errno . ") " . $mysqli->error;
}
/* Prepared statement, stage 2: bind and execute */
$id = 1;
if (!$stmt->bind_param("i", $id)) {
echo "Binding parameters failed: (" . $stmt->errno . ") " .
$stmt->error;
}
if (!$stmt->execute()) {
echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error;
}
因此,下次您需要查找数据库时,请使用参数化查询。但请注意,这种方法也有缺点。在某些情况下,这样做可能会损害性能,因为参数化查询需要服务器资源。在应用程序对性能至关重要的情况下,还有其他方法可以应对 SQL 注入攻击。
存储过程: 这是另一种常用的对抗 SQL 注入攻击的方法。它的工作原理与参数化查询相同,唯一的区别是过程或方法本身存储在数据库中,并在需要时由应用程序调用。以下是如何在 PHP 中为 MySQL 编写存储过程
/* Create the stored procedure */
if (!$mysqli->query("DROP PROCEDURE IF EXISTS p") ||
!$mysqli->query("CREATE PROCEDURE p(IN id_val INT)
↪BEGIN INSERT INTO
test VALUES(id_val); END;")) {
echo "Stored procedure creation failed: (" . $mysqli->errno . ") " .
$mysqli->error;
}
/* Call the stored procedure */
if (!$mysqli->query("CALL p(1)")) {
echo "CALL failed: (" . $mysqli->errno . ") " . $mysqli->error;
}
这种方法在防止 SQL 注入方面与我之前提到的参数化查询方法同样有效,因此您可以决定哪种方法更适合您的情况。
转义用户提供的输入: 在这种方法中,用户输入会被手动(或有时借助 DBMS 转义机制)转义为有效字符串,从而最大限度地减少 SQL 注入攻击的可能性。虽然它比其他方法稍弱,但在您想要更好的性能或正在重写旧代码并希望以更少的精力完成时,它可能很有用。
PHP 提供了一种自动输入转义机制,称为 magic_quotes_gpc,您可以在将输入发送到后端之前使用它。但是,最好使用数据库提供的转义机制,因为最终查询会到达数据库,并且数据库会更了解什么是有效查询。MySQL 提供了 mysql_real_escape_string() 方法来转义输入。查看您的数据库文档以查找支持哪些转义函数。
会话处理一旦合法用户使用他们的凭据登录到站点,就会启动并维护一个会话,直到他们注销。当有人冒充真正的用户试图潜入时,问题就开始了。显然,结果可能非常严重——用户的钱甚至他们的身份都可能被盗。让我们探讨一下如何更改您的编码风格,以便安全地处理会话。
会话管理实现: 您应始终使用 Web 开发框架开箱即用的内置会话管理功能。这不仅节省了关键的开发时间和成本,而且通常也更安全,因为许多人都在使用和测试它。
在实现会话管理时,需要注意的另一件事是跟踪应用程序使用哪种方法来发送/接收会话 ID。它可能是 cookie 或 URL 重写,或者两者兼而有之,但您通常应该限制它,并且仅通过您首先选择的机制接受会话 ID。
Cookie 管理: 每当您计划使用 cookie 时,请注意您正在发送有关用户/会话的数据,这些数据可能会被拦截和滥用。因此,您在处理 cookie 时需要格外小心。始终使用 HttpOnly 安全地添加 cookie 属性,因为这可确保 cookie 始终仅通过 HTTPS 连接发送,并且不允许脚本访问 cookie。这两个属性将减少 cookie 被拦截的机会。
应始终设置域和路径等其他属性,以指示浏览器应将 cookie 发送到何处,以便它仅到达准确的目标位置,而不是其他任何地方。
最后但绝对不是最不重要的,应设置过期和最大年龄属性,以使 cookie 成为非持久性的,以便在浏览器实例关闭后将其擦除。
会话过期管理: 应该在空闲超时之上强制执行超时,以便如果用户打算停留更长时间,他们应该再次验证自己的身份。这会生成一个新的会话 ID,因此攻击者破解会话 ID 的时间更少。
此外,当会话无效时,应特别注意清除浏览器数据和 cookie(如果使用)。要使 cookie 无效,请将会话 ID 设置为空,并将过期日期设置为过去的日期。同样,服务器端也应通过调用适当的会话处理方法来关闭会话并使其无效,例如 PHP 中的 session_destroy()/unset()。
Web 服务安全用外行的话来说,Web 服务可以定义为旨在支持两个电子设备通过 Internet 进行通信的软件系统。然而,在现实生活中,情况并非如此简单。随着 Internet 的发展,滥用这些服务的威胁也在增加。让我们看看您在开发 Web 服务时应牢记的一些重要提示。
模式验证: 无论您期望 Web 服务处理哪些 SOAP 有效负载,都应根据关联的 XML 模式定义对其进行验证。XSD 至少应定义 Web 服务中允许的每个参数的最大长度和字符集。此外,如果您期望使用固定格式的参数,例如电子邮件 ID、电话号码等,请在 XSD 中定义验证模式。
XML 拒绝服务预防: 拒绝服务攻击试图用大量请求淹没 Web 服务器,使其最终崩溃。为了保护您的 Web 服务免受此类攻击,请务必优化最大消息吞吐量的配置并限制 SOAP 消息大小。此外,验证请求是否包含递归或超大有效负载,因为此类有效负载通常可能是恶意的。
安全 URL 重定向很多时候,您需要根据用户输入重定向到新页面。但是,如果处理不当,重定向可能会变得危险。例如,如果攻击者可以重定向应用程序到他们选择的 URL,那么他们就可以发起网络钓鱼攻击并泄露用户数据。安全的 URL 重定向是指您在代码中硬编码 URL,如下所示
<?php header("Location: http://www.mywebsite.com") ?>
应避免以下情况,即您必须转到用户传递的 URL
<?php $url = $_GET['inputURL'];
header("Location: " . $url); ?>
如果您无法避免这种情况,请继续阅读。
不要使用 URL 作为输入: 即使您想根据用户输入重定向到 URL,最好也不要允许用户输入 URL。相反,您可以使用其他输入机制,如按钮或直接链接。这样,您可以防止用户输入任何随机链接。
在重定向之前验证输入: 在您根本无法避免用户输入的情况下,请确保针对完全相同的站点或主机列表或正则表达式验证输入。此外,通知用户他们将被重定向到的站点。
跨站脚本此类攻击的目标是最终用户的浏览器。一种常见的攻击方式是将恶意脚本(以 JavaScript、Flash 甚至 HTML 的形式)注入到真正的 Web 站点。当用户访问该站点时,浏览器不知道脚本是否是真实的,只是执行它。此类脚本一旦执行,就可以访问 cookie、会话数据或存储在浏览器中的其他敏感信息,因为它们是通过用户尝试访问的真实 Web 站点发送的。
为了应对 Web 站点中的此类攻击/注入,OWASP 建议将 Web 页面视为具有某些用于不受信任数据的槽位的模板。例如,假设您正在创建一个主页,并且在左上角,您有一个用于用户名的槽位,该槽位由应用程序检索并在 Web 页面呈现时显示给用户。这些槽位可以用于多个组件之一——例如,JavaScript、HTML 或 CSS。对于每个组件,都有预防措施,以帮助确保其他人无法注入他们的代码。让我们看看所有规则。
首先,您需要定义 Web 页面中应存在的槽位。然后,确保您不允许任何不受信任的数据进入文档,除非它位于您已定义的槽位之一中。
HTML 标记和属性中的不受信任数据: 当您需要在 HTML 标记(如 div、p、b、td 等)中插入不受信任的数据时,请确保在使用前对其进行转义。这样,即使攻击者设法发送了他们的代码,转义不受信任的数据也将确保代码不会造成太大危害。例如,字符 <、>、& 等应分别更改为 <、> 和 &。此外,HTML 标记(如 name、id、width 等)中的属性字段有时可能需要采用可变值,因此您可能需要将不受信任的数据传递给此类字段。在这种情况下,请确保在使用前也对数据进行转义。查看 HTML 实体转义和取消转义的 ESAPI 参考实现。以下是 API 的示例用法
String safe = ESAPI.encoder().encodeForHTMLAttribute(
↪request.getParameter("input" ) );
CSS 中的不受信任数据: 在您需要在 CSS 中放置不受信任数据的情况下,重要的是要注意它只能在属性值中完成,而不能在其他任何地方完成。特别是当您需要传递 URL 时,请检查它是否以 "http" 开头。然后,除了字母数字字符外,转义所有 ASCII 值小于 256 的其他字符(ESAPI 也支持 CSS 转义和取消转义)
String safe = ESAPI.encoder().encodeForCSS(
↪request.getParameter( "input" ) );
HTTP GET 参数中的不受信任数据: 如果在执行之前未转义 GET 参数,则诸如 "http:www.mysite.com/value=data" 之类的 URL 可能是攻击的目标。在这种情况下,请确保使用 %HH 转义格式转义所有 ASCII 值小于 256 的字符。用于 URL 验证的 ESAPI 如下所示
String safe = ESAPI.encoder().encodeForURL(
↪request.getParameter( "input" ) );
JavaScript 数据值中的不受信任数据: 如果您想在 JavaScript 中放置不受信任的数据,唯一安全的位置是在带引号的数据值内。因为否则很容易更改执行上下文并执行实际上应该是数据语句。同样,要遵循的转义机制与以前的情况相同——即,转义所有 ASCII 值小于 256 的字符。用于此目的的 ESAPI 是
String safe = ESAPI.encoder().encodeForJavaScript(
↪request.getParameter("input" ) );
结论
最后,如果您是 Web 开发人员或计划成为 Web 开发人员,最好熟悉 OWASP 安全指南。这不仅可以为您节省大量的设计更改和安全加固工作,还可以确保最终用户的数据和身份安全。