实例分析10个PHP常见安全问题

相对于其他几种语言来说, PHP 在 web 建站方面有更大的优势,即使是新手,也能很容易搭建一个网站出来。但这种优势也容易带来一些负面影响,因为很多的 PHP 教程没有涉及到安全方面的知识。

本文分为几部分,每部分会涵盖不同的安全威胁和应对策略。但是,这并不是说你做到这几点以后,就一定能避免你的网站出现任何问题。如果你想提高你的网站安全性的话,你应该继续通过阅读书籍或者文章,来研究如何提高你的网站安全性

出于演示需要,代码可能不是很完美。日常开发过程中,很多代码都包含在了框架跟各种库里面。作为一个后台开发,你不仅要熟练基本的 CURD,更要知道如何保护你的数据。

1. SQL 注入

我赌一包辣条,你肯定会看到这里。 SQL 注入是对您网站最大的威胁之一,如果您的数据库受到别人的 SQL 注入的攻击的话,别人可以转出你的数据库,也许还会产生更严重的后果。

网站要从数据库中获取动态数据,就必须执行 SQL 语句,举例如下:

<?php

$username = $_GET['username'];

$query = "SELECT * FROM users WHERE username = '$username'";

攻击者控制通过 GET 和 POST 发送的查询(或者例如 UA 的一些其他查询)。一般情况下,你希望查询户名为「 peter 」的用户产生的 SQL 语句如下:

SELECT * FROM users WHERE username = 'peter'

但是,攻击者发送了特定的用户名参数,例如:' OR '1'='1

这就会导致 SQL 语句变成这样:

SELECT * FROM users WHERE username = 'peter' OR '1' = '1'

这样,他就能在不需要密码的情况下导出你的整个用户表的数据了。

那么,我们如何防止这类事故的发生呢?主流的解决方法有两种。转义用户输入的数据或者使用封装好的语句。转义的方法是封装好一个函数,用来对用户提交的数据进行过滤,去掉有害的标签。但是,我不太推荐使用这个方法,因为比较容易忘记在每个地方都做此处理。

下面,我来介绍如何使用 PDO 执行封装好的语句( mysqi 也一样):

$username = $_GET['username'];

$query = $pdo->prepare('SELECT * FROM users WHERE username = :username');

$query->execute(['username' => $username]);

$data = $query->fetch();

动态数据的每个部分都以:做前缀。然后将所有参数作为数组传递给执行函数,看起来就像 PDO 为你转义了有害数据一样。

几乎所有的数据库驱动程序都支持封装好的语句,没有理由不使用它们!养成使用他们的习惯,以后就不会忘记了。

2. XSS

XSS 又叫 CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。

下面以一个搜索页面为例子:

<body>

<?php

$searchQuery = $_GET['q'];

/* some search magic here */

?>

<h1>You searched for: <?php echo $searchQuery; ?></h1>

<p>We found: Absolutely nothing because this is a demo</p>

</body>

因为我们把用户的内容直接打印出来,不经过任何过滤,非法用户可以拼接 URL:

search.php?q=%3Cscript%3Ealert(1)%3B%3C%2Fscript%3E

PHP 渲染出来的内容如下,可以看到 Javascript 代码会被直接执行:

<body>

<h1>You searched for: <script>alert(1);</script></h1>

<p>We found: Absolutely nothing because this is a demo</p>

</body>

问:JS 代码被执行有什么大不了的?

Javascript 可以:

偷走你用户浏览器里的 Cookie;

通过浏览器的记住密码功能获取到你的站点登录账号和密码;

盗取用户的机密信息;

你的用户在站点上能做到的事情,有了 JS 权限执行权限就都能做,也就是说 A 用户可以模拟成为任何用户;

在你的网页中嵌入恶意代码;

...

问:如何防范此问题呢?

好消息是比较先进的浏览器现在已经具备了一些基础的 XSS 防范功能,不过请不要依赖与此。

正确的做法是坚决不要相信用户的任何输入,并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的 XSS 攻击:

<?php

$searchQuery = htmlentities($searchQuery, ENT_QUOTES);

或者你可以使用模板引擎 Twig ,一般的模板引擎都会默认为输出加上 htmlentities 防范。

如果你保持了用户的输入内容,在输出时也要特别注意,在以下的例子中,我们允许用户填写自己的博客链接:

<body>

 <a href="<?php echo $homepageUrl; ?>" rel="external nofollow" >Visit Users homepage</a>

</body>

以上代码可能第一眼看不出来有问题,但是假设用户填入以下内容:

#" onclick="alert(1)

会被渲染为:

<body>

 <a href="#" rel="external nofollow" onclick="alert(1)">Visit Users homepage</a>

</body>

永远永远不要相信用户输入的数据,或者,永远都假设用户的内容是有攻击性的,态度端正了,然后小心地处理好每一次的用户输入和输出。

另外设置 Cookie 时,如果无需 JS 读取的话,请必须设置为 "HTTP ONLY"。这个设置可以令 JavaScript 无法读取 PHP 端种的 Cookie。

3. XSRF/CSRF

CSRF 是跨站请求伪造的缩写,它是攻击者通过一些技术手段欺骗用户去访问曾经认证过的网站并运行一些操作。

虽然此处展示的例子是 GET 请求,但只是相较于 POST 更容易理解,并非防护手段,两者都不是私密的 Cookies 或者多步表单。

假如你有一个允许用户删除账户的页面,如下所示:

<?php

//delete-account.php

$confirm = $_GET['confirm'];

if($confirm === 'yes') {

 //goodbye

}

攻击者可以在他的站点上构建一个触发这个 URL 的表单(同样适用于 POST 的表单),或者将 URL 加载为图片诱惑用户点击:

<img src="https://example.com/delete-account.php?confirm=yes" />

用户一旦触发,就会执行删除账户的指令,眨眼你的账户就消失了。

防御这样的攻击比防御 XSS 与 SQL 注入更复杂一些。

最常用的防御方法是生成一个 CSRF 令牌加密安全字符串,一般称其为 Token,并将 Token 存储于 Cookie 或者 Session 中。

每次你在网页构造表单时,将 Token 令牌放在表单中的隐藏字段,表单请求服务器以后会根据用户的 Cookie 或者 Session 里的 Token 令牌比对,校验成功才给予通过。

由于攻击者无法知道 Token 令牌的内容(每个表单的 Token 令牌都是随机的),因此无法冒充用户。

<?php /* 你嵌入表单的页面 */ ?>

<form action="/delete-account.php" method="post">

 <input type="hidden" name="csrf" value="<?php echo $_SESSION['csrf']; ?>">

 <input type="hidden" name="confirm" value="yes" />

 <input type="submit" value="Delete my account" />

</form>

## 

<?php

//delete-account.php

$confirm = $_POST['confirm'];

$csrf = $_POST['csrf'];

$knownGoodToken = $_SESSION['csrf'];

if($csrf !== $knownGoodToken) {

 die('Invalid request');

}

if($confirm === 'yes') {

 //goodbye

}

请注意,这是个非常简单的示例,你可以加入更多的代码。如果你使用的是像 Symfony 这样的 PHP 框架,那么自带了 CSRF 令牌的功能。

4. LFI

LFI (本地文件包含) 是一个用户未经验证从磁盘读取文件的漏洞。

我经常遇到编程不规范的路由代码示例,它们不验证过滤用户的输入。我们用以下文件为例,将它要渲染的模板文件用 GET 请求加载。

<body>

<?php

 $page = $_GET['page'];

 if(!$page) {

  $page = 'main.php';

 }

 include($page);

?>

</body>

由于 Include 可以加载任何文件,不仅仅是 PHP,攻击者可以将系统上的任何文件作为包含目标传递。

index.php?page=../../etc/passwd

这将导致 /etc/passwd 文件被读取并展示在浏览器上。

要防御此类攻击,你必须仔细考虑允许用户输入的类型,并删除可能有害的字符,如输入字符中的 “.” “/” “\”。

如果你真的想使用像这样的路由系统(我不建议以任何方式),你可以自动附加 PHP 扩展,删除任何非 [a-zA-Z0-9-_] 的字符,并指定从专用的模板文件夹中加载,以免被包含任何非模板文件。

我在不同的开发文档中,多次看到造成此类漏洞的 PHP 代码。从一开始就要有清晰的设计思路,允许所需要包含的文件类型,并删除掉多余的内容。你还可以构造要读取文件的绝对路径,并验证文件是否存在来作为保护,而不是任何位置都给予读取。

5. 不充分的密码哈希

大部分的 Web 应用需要保存用户的认证信息。如果密码哈希做的足够好,在你的网站被攻破时,即可保护用户的密码不被非法读取。

首先,最不应该做的事情,就是把用户密码明文储存起来。大部分的用户会在多个网站上使用同一个密码,这是不可改变的事实。当你的网站被攻破,意味着用户的其他网站的账号也被攻破了。

其次,你不应该使用简单的哈希算法,事实上所有没有专门为密码哈希优化的算法都不应使用。哈希算法如 MD5 或者 SHA 设计初衷就是执行起来非常快。这不是你需要的,密码哈希的终极目标就是让黑客花费无穷尽的时间和精力都无法破解出来密码。

另外一个比较重要的点是你应该为密码哈希加盐(Salt),加盐处理避免了两个同样的密码会产生同样哈希的问题。

以下使用 MD5 来做例子,所以请千万不要使用 MD5 来哈希你的密码, MD5 是不安全的。

假如我们的用户 user1 和 user315 都有相同的密码 ilovecats123,这个密码虽然看起来是强密码,有字母有数字,但是在数据库里,两个用户的密码哈希数据将会是相同的:5e2b4d823db9d044ecd5e084b6d33ea5 。

如果一个如果黑客拿下了你的网站,获取到了这些哈希数据,他将不需要去暴力破解用户 user315 的密码。我们要尽量让他花大精力来破解你的密码,所以我们对数据进行加盐处理:

<?php

//warning: !!这是一个很不安全的密码哈希例子,请不要使用!!

$password = 'cat123';

$salt = random_bytes(20);

$hash = md5($password . $salt);

最后在保存你的唯一密码哈希数据时,请不要忘记连 $salt 也已经保存,否则你将无法验证用户。

在当下,最好的密码哈希选项是 bcrypt,这是专门为哈希密码而设计的哈希算法,同时这套哈希算法里还允许你配置一些参数来加大破解的难度。

新版的 PHP 中也自带了安全的密码哈希函数 password_hash ,此函数已经包含了加盐处理。对应的密码验证函数为 password_verify 用来检测密码是否正确。password_verify 还可有效防止 时序攻击.

以下是使用的例子:

<?php

//user signup

$password = $_POST['password'];

$hashedPassword = password_hash($password, PASSWORD_DEFAULT);

//login

$password = $_POST['password'];

$hash = '1234'; //load this value from your db

if(password_verify($password, $hash)) {

 echo 'Password is valid!';

} else {

 echo 'Invalid password.';

}

需要澄清的一点是:密码哈希并不是密码加密。哈希(Hash)是将目标文本转换成具有相同长度的、不可逆的杂凑字符串(或叫做消息摘要),而加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。显然他们之间最大的区别是可逆性,在储存密码时,我们要的就是哈希这种不可逆的属性。

6. 中间人攻击

MITM (中间人) 攻击不是针对服务器直接攻击,而是针对用户进行,攻击者作为中间人欺骗服务器他是用户,欺骗用户他是服务器,从而来拦截用户与网站的流量,并从中注入恶意内容或者读取私密信息,通常发生在公共 WiFi 网络中,也有可能发生在其他流量通过的地方,例如 ISP 运营商。

对此的唯一防御是使用 HTTPS,使用 HTTPS 可以将你的连接加密,并且无法读取或者篡改流量。你可以从 Let's Encrypt 获取免费的 SSL 证书,或从其他供应商处购买,这里不详细介绍如何正确配置 WEB 服务器,因为这与应用程序安全性无关,且在很大程度上取决于你的设置。

你还可以采取一些措施使 HTTPS 更安全,在 WEB 服务器配置加上 Strict-Transport-Security 标示头,此头部信息告诉浏览器,你的网站始终通过 HTTPS 访问,如果未通过 HTTPS 将返回错误报告提示浏览器不应显示该页面。

然而,这里有个明显的问题,如果浏览器之前从未访问过你的网站,则无法知道你使用此标示头,这时候就需要用到 Hstspreload。

可以在此注册你的网站: https://hstspreload.org/

你在此处提交的所有网站都将被标记为仅 HTTPS,并硬编码到 Google Chrome、FireFox、Opera、Safari、IE11 和 Edge 的源代码中。

你还可以在 DNS 配置中添加 Certification Authority Authorization (CAA) record ,可以仅允许一个证书颁发机构(例如: Let's encrypt)发布你的域名证书,这进一步提高了用户的安全性。

7. 命令注入

这可能是服务器遇到的最严重的攻击,命令注入的目标是欺骗服务器执行任意 Shell 命令

你如果使用 shell_exec 或是 exec 函数。让我们做一个小例子,允许用户简单的从服务器 Ping 不同的主机。

<?php

$targetIp = $_GET['ip'];

$output = shell_exec("ping -c 5 $targetIp");

输出将包括对目标主机 Ping 5 次。除非采用 sh 命令执行 Shell 脚本,否则攻击者可以执行想要的任何操作。

ping.php?ip=8.8.8.8;ls -l /etc

Shell 将执行 Ping 和由攻击者拼接的第二个命令,这显然是非常危险的。

感谢 PHP 提供了一个函数来转义 Shell 参数。

escapeshellarg 转义用户的输入并将其封装成单引号。

<?php

$targetIp = escapeshellarg($_GET['ip']);

$output = shell_exec("ping -c 5 $targetIp");

现在你的命令应该是相当安全的,就个人而言,我仍然避免使用 PHP 调用外部命令,但这完全取决于你自己的喜好。

另外,我建议进一步验证用户输入是否符合你期望的形式。

8. XXE

XXE (XML 外部实体) 是一种应用程序使用配置不正确的 XML 解析器解析外部 XML 时,导致的本地文件包含攻击,甚至可以远程代码执行。

XML 有一个鲜为人知的特性,它允许文档作者将远程和本地文件作为实体包含在其 XML 文件中。

<?xml version="1.0" encoding="ISO-8859-1"?>

 <!DOCTYPE foo [

  <!ELEMENT foo ANY >

  <!ENTITY passwd SYSTEM "file:///etc/passwd" >]>

  <foo>&passwd;</foo>

就像这样, /etc/passwd 文件内容被转储到 XML 文件中。

如果你使用 libxml 可以调用 libxml_disable_entity_loader 来保护自己免受此类攻击。使用前请仔细检查 XML 库的默认配置,以确保配置成功。

9. 在生产环境中不正确的错误报告暴露敏感数据

如果你不小心,可能会在生产环境中因为不正确的错误报告泄露了敏感信息,例如:文件夹结构、数据库结构、连接信息与用户信息。

你是不希望用户看到这个的吧?

一般根据你使用的框架或者 CMS ,配置方法会有不同的变化。通常框架具有允许你将站点更改为某种生产环境的设置。这样会将所有用户可见的错误消息重定向到日志文件中,并向用户显示非描述性的 500 错误,同时允许你根据错误代码检查。

但是你应该根据你的 PHP 环境设置: error_reporting 与 display_errors.

10. 登录限制

像登录这样的敏感表单应该有一个严格的速率限制,以防止暴力攻击。保存每个用户在过去几分钟内失败的登录尝试次数,如果该速率超过你定义的阈值,则拒绝进一步登录尝试,直到冷却期结束。还可通过电子邮件通知用户登录失败,以便他们知道自己的账户被成为目标。

(0)

相关推荐

  • PHP弱类型的安全问题详细总结

    前言 相信大家都知道PHP是世界上最好的语言,PHP本身的问题也可以算作是web安全的一个方面.在PHP中的特性就是弱类型,以及内置函数对于传入参数的松散处理. 这篇文章主要就是记录我在做攻防平台上面遇到的PHP的函数中存在的问题,以及PHP的弱类型所带来的问题.对大家在学习或者使用php的时候具有一定的参考借鉴价值,下面来一起看看. PHP弱类型简介 小编之所以认为php很强大是因为php提供了很多独有的特性工开发者使用,其中一个就是php弱类型机制. 在PHP中,可以进行一下的操作. $pa

  • 详解php比较操作符的安全问题

    php的比较操作符有==(等于)松散比较,===(完全等于)严格比较,这里面就会引入很多有意思的问题. 在松散比较的时候,php会将他们的类型统一,比如说字符到数字,非bool类型转换成bool类型,为了避免意想不到的运行效果,应该使用严格比较.如下是php manual上的比较运算符表: 例子 名称 结果 $a == $b 等于 TRUE,如果类型转换后 $a 等于 $b. $a === $b 全等 TRUE,如果 $a 等于 $b,并且它们的类型也相同. $a != $b 不等 TRUE,如

  • php session安全问题分析

    因此,我们主要解决的思路是效验session ID的有效性. 以下为引用的内容: 复制代码 代码如下: <?php if(!isset($_SESSION['user_agent'])){ $_SESSION['user_agent'] =$_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT']; } /* 如果用户session ID是伪造 */ elseif ($_SESSION['user_agent'] != $_SERVER['REMOTE

  • php中文件上传的安全问题

    可以读/etc/passwd! 这段.. [文件上载] PHP自动支持基于RFC 1867的文件上载,我们看下面的例子: <FORM METHOD="POST" ENCTYPE="multipart/form-data"> <INPUT TYPE="FILE" NAME="hello"> <INPUT TYPE="HIDDEN" NAME="MAX_FILE_SIZE

  • 不要轻信 PHP_SELF的安全问题

    复制代码 代码如下: <html> <body> <?php if (isset($_REQUEST['submitted']) && $_REQUEST['submitted'] == '1') { echo "Form submitted!"; } ?> <form action="<?php echo $_SERVER['PHP_SELF']; ?>"> <input type=

  • PHP开发需要注意的安全问题

    作为PHP程序员,特别是新手,对于互联网的险恶总是知道的太少,对于外部的入侵有很多时候是素手无策的,他们根本不知道黑客是如何入侵的.提交入侵.上传漏洞.sql 注入.跨脚本攻击等等.作为最基本的防范你需要注意你的外部提交,做好第一面安全机制处理防火墙. 规则 1:绝不要信任外部数据或输入 关于Web应用程序安全性,必须认识到的第一件事是不应该信任外部数据.外部数据(outside data) 包括不是由程序员在PHP代码中直接输入的任何数据.在采取措施确保安全之前,来自任何其他来源(比如 GET

  • 实例分析10个PHP常见安全问题

    相对于其他几种语言来说, PHP 在 web 建站方面有更大的优势,即使是新手,也能很容易搭建一个网站出来.但这种优势也容易带来一些负面影响,因为很多的 PHP 教程没有涉及到安全方面的知识. 本文分为几部分,每部分会涵盖不同的安全威胁和应对策略.但是,这并不是说你做到这几点以后,就一定能避免你的网站出现任何问题.如果你想提高你的网站安全性的话,你应该继续通过阅读书籍或者文章,来研究如何提高你的网站安全性 出于演示需要,代码可能不是很完美.日常开发过程中,很多代码都包含在了框架跟各种库里面.作为

  • PHP中常见的缓存技术实例分析

    本文实例分析了PHP中常见的缓存技术.分享给大家供大家参考.具体如下: JBLOG在开发的过程中,对性能的优化做了不少工作.为了尽量减少不必要的数据库查询,我对一些数据进行了缓存和静态化处理. 缓存的原理:把一些经常要用到但又很少改动的数据以数组或其它形式存储到一个独立的PHP文件中,然后在需要用到的时候包含进来. 缓存的优点:能够大大减少数据库的查询次数,减轻数据库的压力,提高程序的执行效率. JBLOG缓存的数据有:系统设置.博客分类.侧栏最新日志.最新评论.博客统计.日志归档.友情链接.标

  • python迭代器常见用法实例分析

    本文实例讲述了python迭代器常见用法.分享给大家供大家参考,具体如下: 迭代器 迭代是访问集合元素的一种方式.迭代器是一个可以记住遍历的位置的对象.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退. 1. 可迭代对象 我们已经知道可以对list.tuple.str等类型的数据使用for-in-的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代. 但是,是否所有的数据类型都可以放到for-in-的语句中,然后让for-in-每次从中

  • JavaScript定时器常见用法实例分析

    本文实例讲述了JavaScript定时器常见用法.分享给大家供大家参考,具体如下: 定时器 定时器在javascript中的作用 1.制作动画 2.异步操作 3.函数缓冲与节流 定时器类型及语法 /* 定时器: setTimeout 只执行一次的定时器 clearTimeout 关闭只执行一次的定时器 setInterval 反复执行的定时器 clearInterval 关闭反复执行的定时器 */ var time1 = setTimeout(myalert,2000); var time2 =

  • Python文件常见操作实例分析【读写、遍历】

    本文实例讲述了Python文件常见操作.分享给大家供大家参考,具体如下: 1.文件是什么? 文件是存储在外部介质上的数据或信息集合,程序中源程序.数据中保存的数据.图像中的像素数据等等: 文件是有序的数据序列: 2.编码: 信息从一种形式转换为另一种形式的过程:ascII码.Unicode.utf-8等等: 二进制文件ascII码:照片.音乐.视频.计算机程序... 3.文件的基本操作 1)打开文件获取文件对象 建立磁盘上的文件与源程序中的对象相关联,程序上叫打开文件,一旦文件被打开,文件的内容

  • mysql数据库常见基本操作实例分析【创建、查看、修改及删除数据库】

    本文实例讲述了mysql数据库常见基本操作.分享给大家供大家参考,具体如下: 本节相关: 创建数据库 查看数据库 修改数据库 删除数据库 首发时间:2018-02-13 20:47 修改: 2018-04-07:考虑到规范化,将所有语法中"关键字"变成大写;以及因为整理"mysql学习之路",移除字符集和校对集问题并归成一个新博文. 创建数据库  : 语法  : CREATE DATABASE 数据库名字[库选项]; 库选项说明  : 库选项是可选项,可以不写 ,如

  • PHP多线程之内部多线程实例分析

    本文实例分析了PHP多线程之内部多线程用法.分享给大家供大家参考.具体如下: 复制代码 代码如下: <?php class Http_MultiRequest {     //要并行抓取的url 列表     private $urls = array();     //curl 的选项     private $options;     //构造函数     function __construct($options = array())     {         $this->setOp

  • PHP数组操作实例分析【添加,删除,计算,反转,排序,查找等】

    本文实例分析了PHP数组操作.分享给大家供大家参考,具体如下: PHP的数组是很重要的一部分.操作示例如下: <?php function br() { echo '<br />===============================================<br />'; } $arr1 = array(); $arr1[] = 'x'; $arr1[] = 'a'; $arr1[] = 'e'; $arr1[] = 'c'; $arr1[] = 'h'; /

  • PHP计算日期相差天数实例分析

    本文实例分析了PHP计算日期相差天数的方法.分享给大家供大家参考,具体如下: <?PHP //今天与2016年10月27日相差多少天 $Date_1=date("Y-m-d"); $Date_2="2016-10-27"; $d1=strtotime($Date_1); $d2=strtotime($Date_2); $Days=round(($d1-$d2)/3600/24); echo "今天与2016年10月27日相差".$Days.

  • MySQL索引用法实例分析

    本文实例分析了MySQL索引用法.分享给大家供大家参考,具体如下: MYSQL描述: 一个文章库,里面有两个表:category和article.category里面有10条分类数据.article里面有20万条.article里面有一个"article_category"字段是与category里的"category_id"字段相对应的.article表里面已经把 article_category字义为了索引.数据库大小为1.3G. 问题描述: 执行一个很普通的查

随机推荐