调用如下的HtmlEncode 函数进行编码,php 示例代码,其他语言可以参照实现。
function HtmlEncode($str)
{
$str = str_replace("&","&", $str);
$str = str_replace(">",">", $str);
$str = str_replace("<","<", $str);
$str = str_replace("\"",""", $str);
$str = str_replace("'","'", $str);
$str = preg_replace("/\s+/"," ", $str);
return $str;
}
注:为了预防业务将参数输出至属性值时未使用引号包裹的情况,这里自己实现的HtmlEncode 也将空格编码。
一般来说,如果变量是完整的url,应该先检查下是否以^http开头,且目的域名是允许的域名列表内,以保证不会出现伪协议类的XSS攻击。接着使用 HtmlEncode编码下;或者因为此处是地址栏,可以用 php标准函数urlencode 编码变量。
使用 jsencode(\uUUUU) 编码,php 示例代码,其他语言可以参照实现。
function jsencode($str)
{
$arr = array();
$str_len = strlen($str);
$need_encode = "<>\"&#`()[]';";
for($i = 0; $i < $str_len; $i++)
{
if (strpos($need_encode, $str[$i]))
{
$arr[$i] = "\\u00" . bin2hex($str[$i]);
}
else
{
$arr[$i] = $str[$i];
}
}
return join("", $arr);
}
参考 这里 进行标签和属性的白名单过滤。
同反射XSS,在输出变量时参照如上情形描述。
在"$var" 输出到<script>
时,应该执行一次 jsencode(\uUUUU);其次,在document.write 输出到页面 html 页面时,要分具体情况对待:如果是输出到事件或者脚本,则要再做一次 jsEncode;如果是输出到 html 内容或者属性,则要再做一次 HtmlEncode。
如下情况属于后者,即 HtmlEncode (jsencode($var))
<script>
var x = "$var";
document.write("<a href='"+x+"' >test</a>");
</script>
Javascript 函数代码示例如下:
function jsencode(str) {
var arr = [];
var need_encode = "<>\"&#`()[]';";
for (var i = 0; i < str.length; i++) {
if (need_encode.indexOf(str[i]) != -1)
{
arr[i] = ("\\u00" + str.charCodeAt(i).toString(16)).slice(-6);
}
else
{
arr[i] = str[i];
}
}
return arr.join("");
}
function HtmlEncode(sStr)
{
sStr = sStr.replace(/&/g,"&");
sStr = sStr.replace(/>/g,">");
sStr = sStr.replace(/</g,"<");
sStr = sStr.replace(/"/g,""");
sStr = sStr.replace(/'/g,"'");
sStr = sStr.replace(/\s+/g," ");
return sStr;
}
在使用location.href / location.replace() / location.assign() 等函数实现前端跳转时不止要考虑url 任意跳转的问题,如1.1.2 所说也要避免伪协议引起的xss攻击。
建议不再使用的swf请下线,其他有漏洞的swf请更新至官方最新版,或者用其他功能类似swf 替代实现。
Refer 校验需要严格的正则,比如 /^http://www.qq.com/index.html$/
,否则可能被绕过。但是某些情况下发出的情况Refer 为空甚至以前曾经出现过flash请求可以伪造Refer,故更敏感的一些操作建议使用验证token方式。
即在页面埋下隐藏的动态token值,后端逻辑对这个token 值进行验证,如果不正确则直接退出。php 代码示例如下:
<?php
session_start();
if (isset($_SESSION['token']) && isset($_POST['username'])) {
if (empty($_POST['token']) || $_POST['token'] != $_SESSION['token']) {
header('Location: http://localhost/error.php?token=' . $_SESSION['token']);
exit(-1);
}
}
$token = md5(time());
$_SESSION['token'] = $token;
?>
<html>
<head>
<title>test with csrf token</title>
</head>
<body>
<form method="post" action="/csrf.php" id="form">
<div>
<input name="username" type="text" id="username" style="width:270px;"/>
<input type="submit" name="btnSearch" value="Search" id="btnSearch" />
<input type="hidden" name="token" id="token" value="<?php echo $token;?>" />
</div>
</form>
<br/>
</body>
</html>
在特别关键的操作建议启用,其他操作建议预埋验证码,紧急时刻(如出现CSRF蠕虫)可以临时启用。
PHP5中,增加了MySQL支持,提供了mysqli扩展:
PHP手册地址:http://php.net/mysqli
<?php
// retrieve the user's input
$animalName = $_POST['animalName'];
// connect to the database
$connect = mysqli_connect('localhost', 'username', 'password', 'database');
if (!$connect)
exit('connection failed: ' . mysqli_connect_error());
// create a query statement resource
$stmt = mysqli_prepare($connect, "SELECT intelligence FROM animals WHERE name = ?");
if ($stmt) {
// bind the substitution to the statement
mysqli_stmt_bind_param($stmt, "s", $animalName);
// execute the statement
mysqli_stmt_execute($stmt);
// retrieve the result...
mysqli_stmt_bind_result($stmt, $intelligence);
// ...and display it
if (mysqli_stmt_fetch($stmt)) {
print "A $animalName has $intelligence intelligence.\n";
} else {
print 'Sorry, no records found.';
}
// clean up statement resource
mysqli_stmt_close($stmt);
}
mysqli_close($connect);
?>
如果php版本太低,或者改造代码成本比较大,在使用参数拼接方式实现sql 操作时,务必做到以下两点:
当用户输入的为数字时可以使用如下方式:
使用is_int()函数(或is_integer()或is_long()函数)
使用gettype()函数
使用intval()函数
使用settype()函数
检查用户输入字符串的长度使用strlen()函数。
检查日期或时间是否是有效的,可以使用strtotime()函数。
对于一个已经存在的程序来说,可以写一个通用函数来过滤:
function safe($string)
{
return "'" . mysql_real_escape_string($string) . "'";
}
调用方式:
$variety = safe($_POST[' variety ']);
$query = "SELECT * FROM wines WHERE variety=" . $variety;
注:mysql_real_escape_string 必须在(PHP 4 >= 4.3.0, PHP 5)的情况下才能使用,否则只能用 mysql_escape_string,转义字符有:\x00 , \n , \r , \ , ' , " and \x1a。
Safe 函数中需要将转义之后的字符串用引号包裹起来(对于int类型说查询结果一致),否则黑客本来就不用考虑去闭合引号,转义操作也等于没有效果。
如果数据库字符集是gbk,可能存在宽字节绕过问题,需要在查询之前设置一下character_set_client,如下所示:
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);
php中可以使用 open_basedir 将用户文件访问限制在指定的区域。如将文件访问限制在 /dir/user/ 中,在php.ini中设置 open_basedir = /dir/user/。
对包含文件的名称、路径进行严格限制和过滤(建议白名单方式),避免被篡改为恶意文件,单纯过滤 ”../” 等相对路径串很可能会被绕过。过滤以下字符(以逗号分隔):
../, %2e%2e%2f, %2e%2e/, ..%2f, %2e%2e%5c, %2e%2e\, ..%5c, %252e%252e%255c, ..%c0%af(仅windows需要), ..%c1%9c(仅windows需要)
关闭allow_url_fopen、allow_url_include。
对于传入的变量做过滤,对于 \n $ & ; | ' " ( ) `(反单引号) 过滤或转义这些特殊字符。
- 限制 C(libcurl)/php(cURL) 允许的协议
如设置CURLPROTO_HTTP选项,仅仅允许http和https请求,可以防止类似于file:///, gopher://, ftp:// 等引起的问题。
- 限制访问内网
从url 中提取出域名 www.test.com ,解析域名,获得域名指向的地址 X.X.X.X
检查地址是否为内网地址段:10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8等,
如果是内网地址则屏蔽;
如果不是内网地址,设置 CURLOPT_RESOLVE(libcurl),使libcurl 将域名解析到我们刚才解析出的ip 地址进行访问,即真正发起请求时不再进行dns解析,避免被 dns rebinding 绕过。
- 避免30x 跳转绕过
设置不跟随跳转,即设置 CURLOPT_FOLLOWLOCATION 为false。如果一定要跟随跳转,这里因为使用了库函数,故不能自己在跟随跳转前判断下域名解析的目标ip 是否是内网(如2),但可以通过设置 CURLOPT_OPENSOCKETFUNCTION 回调函数,在socket 创建之前先校验目标地址。
// curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, opensocket_callback);
curl_socket_t opensocket_callback(void *clientp, curlsocktype purpose,
struct curl_sockaddr *addr)
{
struct sockaddr_in *addr_in = (struct sockaddr_in *) &addr->addr;
unsigned int uip = ntohl(addr_in->sin_addr.s_addr);
char ipbuf[50];
inet_ntop(addr_in->sin_family, &addr_in->sin_addr, ipbuf, sizeof(ipbuf));
INF("Connect IP: %s\n", ipbuf);
if ((uip >= 0x7F000000 && uip <= 0x7FFFFFFF) ||
(uip >= 0x0A000000 && uip <= 0x0AFFFFFF) ||
(uip >= 0xAC100000 && uip <= 0xAC1FFFFF) ||
(uip >= 0xC0A80000 && uip <= 0xC0A8FFFF) ||
(uip == 0x00000000 || uip == 0xFFFFFFFF))
{
//过滤来自内网的访问
WRN("target ip is illegal.\n");
return CURL_SOCKET_BAD;
}
return socket(addr->family, addr->socktype, addr->protocol);
}
这样,每当libcurl 试图创建socket 连接某个服务器时,都会先执行 opensocket_callback 回调函数。若目标地址不合法,则可以在这个回调函数中返回CURL_SOCKET_BAD,libcurl 也会因此无法建立连接返回失败。
如果Apache以daemon普通用户启动,则黑客通过网站漏洞入侵服务器后,将获得Apache的daemon权限,因此需要确认网站web目录和文件的属主与Apache启动用户不同,防止网站被黑客恶意篡改和删除。
a) 网站web目录和文件的属主可以设置为root等(非Apache启动用户)。
b) Web目录权限统一设置为755,web文件权限统一设置为644(cgi文件若需执行权限可设置为755),只有上传目录等需要可读可写权限的目录可以设置为777(目录需要有执行权限才可进入)。
目录默认不可写,可写目录不解析,Web Server非root,管理页面不对外。
可以结合使用mime type、后缀方式、文件头部(getimagesize /exif_imagetype),强烈推荐白名单方式。
使用随机数改写文件名和文件路径;或者把文件放在非web 目录下(或者统一的一个文件服务器),且设置open_basedir 以避免被文件包含。
配置文件如下:
<Files ^(*.jpeg|*.jpg|*.png|*.gif)>
order deny,allow
deny from all
</Files>
管理后台默认不对外开放,如果一定要对外开放,需要有安全配置。
对于管理目录,需要做到只允许合法ip(一般内网)可以访问,nginx限制白名单ip访问的配置如下:
location ~ ^/private/ {
allow 192.168.1.0/24;
deny all;
}
管理目录建议启用密码认证,密码认证依靠Web应用自身的认证机制。如果Web应用无认证机制,可启用nginx(apache同理)的密码认证机制,配置如下:
location ^~ /soft/ {
location ~ .*\.(php|php5)?$ {
fastcgi_pass unix:/tmp/php-cgi.sock;#这里按照你自己的设置
fastcgi_index index.php;
include fcgi.conf;
}
auth_basic "Authorized users only";
auth_basic_user_file 这里写前面脚本返回的文件路径;
}