好好学习,天天向上
跨域问题对于我本人来说一直是一个很头疼的问题,因为每次听到什么CORS啊、JSONP啊等等就会不知所措,其实如果从开发的角度深入进去,就是很简单的串门。看了B站蜗牛学苑的视频,终于把跨域完全理解了。接下来,让我们从JSONP、CORS和CSP三个部分,深入理解跨域问题,如果有一起做实验的,希望可以提前准备好两台服务器+每台服务器上部署一个web服务
请把握好这两句话,带着这两句话拿下跨域
1.协议、域名/IP、端口完全一样属于同一个域,任何一个不一样,都是不同域
例如:
http://a.123.com/web/和http://a.123.com/api 同域
http://a.123.com/和https://a.123.com/ 不同域
http://a.123.com/和http:81//a.123.com/ 不同域
http://a.123.com/和http//b.123.com/ 不同域
2.禁止跨域访问,是浏览器帮助我们进行的
本次例子使用两台服务器
192.168.174.5
192.168.174.134
我们先在192.168.174.134发布php,当然这个服务异常简单,连接数据库,获取信息,输出
数据库的sql我在附录中备注,其实可以自己创建一个库,根据sql语句完善库信息
// 连接数据库并访问数据
$conn = new mysqli('127.0.0.1', 'root', '123456', 'users') or die("数据库连接不成功.");
$conn->set_charset('utf8');
// $sql = "select articleid, author, viewcount, createtime from article where articleid<5";
$sql = "select aid, author, create_time from articles where aid<5";
$result = $conn->query($sql);
// 输出JSON数据到页面
$json = json_encode($result->fetch_all(MYSQLI_ASSOC));
echo $json;
// echo $_GET["callback"] . "(" . $json . ")";
$conn->close();
?>
运行一下试试,当然没问题,输出一段JSON格式的数据
紧接着,我在192.168.174.134上,发布一个html,这个html就是向上面的php发送请求读取那一段JSON格式的数据并alert输出,请问这一步是否能成功呢?
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>List-JSONPtitle>
<script type="text/javascript">
var listUrl = 'http://192.168.174.134:82/list-json.php';
// 实例化XMLHttpRequest,用于发送AJAX请求
xmlhttp = new XMLHttpRequest();
var count = 0;
// 当请求的状态发生变化时,触发执行代码
xmlhttp.onreadystatechange=function() {
if(xmlhttp.readyState ==4 && xmlhttp.status==200)
{
// 取得请求的响应,并从响应中通过正则提取Token
var text = xmlhttp.responseText;
alert(text);
}
};
xmlhttp.open("GET",listUrl, false);
xmlhttp.send();
script>
head>
<body>
body>
html>
当然没问题,自己读自己,同一个域当然可以访问
那么这次我们在另一台192.168.174.5上发布这个html去读取192.168.174.134的数据呢,这样是否可以读取到呢?注意最关键的URL
访问一下试试看,界面空白无所谓,也没有弹窗,只能F12看一下
有着对于安全十分重要的三条告警,当然我这个是汉化了,很明显,同源策略禁止读取
请深刻记住此时的业务场景,虽然简单但是已经可以说明问题了
http://192.168.174.5:82/list-json.html访问http://192.168.174.134:82/list-json.php
浏览器报错已拦截跨源请求:同源策略禁止读取位于 http://192.168.174.134:82/list-json.php 的远程资源。(原因:CORS 头缺少 'Access-Control-Allow-Origin')。
其实也很明显,上面第一点我们才说到”三元组“,其中IP变了,自然就跨域了,既然跨域了,第二点浏览器报出的异常,是浏览器做了拦截,为了验证这一点,我们拿出fiddler先访问不涉及跨域的
http://192.168.174.134:82/list-json.html
可以看到两个请求,先访问的html,html再去请求php,当然第二个请求也返回了数据
我们再次访问涉及跨域的
http://192.168.174.5:82/list-json.html
可以看到第一个请求没有什么区别
重点就在第二个请求上,第一个请求我们正常访问html,是再正常不过的访问,
但是从http://192.168.174.5:82/list-json.html访问http://192.168.174.134:82/list-json.php的时候,由于我们是在浏览器上访问,浏览器帮我们加上了一个请求头
origin: http://192.168.174.5:82
表示这个请求是从http://192.168.174.5:82来的,浏览器拿到响应之后,就在响应头里面找Access-Control-Allow-Origin在哪?Access-Control-Allow-Origin里面有没有允许http://192.168.174.5:82,结果响应里面没有这个字段Access-Control-Allow-Origin,浏览器就会返回刚刚看到的那三条异常提示
但是我们要注意响应里面已经返回了JSON格式的数据,这又说明什么呢?结论就是请求正常发送,响应正常收到,数据拿到,但是经过浏览器渲染的时候,进行的拦截,所以禁止跨域访问是浏览器自身的安全机制。
我们总结一下整个过程,A域访问B域->浏览器增加origin:A域->B域给响应->浏览器判断响应里面Access-Control-Allow-Origin是否允许了A域,没有允许就报错
相信到了这一步,大家对跨域都有了了解
那么,我现在再换台电脑换另个IP,使用python直接往http://192.168.174.134:82/list-json.php发送请求获取这个数据,是否可以正常访问呢?
应该不会有人说否吧?
跨域是浏览器的安全机制,我用python发请求,根本不会经过浏览器,所以压根就没有origin的事儿
答案当然是可以访问,所以一定要牢记最开始说的两点
这个时候可能会有疑问,那用户都像我们这样用python/php写个脚本,或着自己抓包修改把请求响应里面改一下不就能访问了,当然能访问了,但是但凡一个正常的用户99%不会像安全人员一样,在输入框输入select的,正常用户都是在浏览器上进行操作,压根不会考虑说我抓个包改一改,或者直接给后台发请求,所以禁止跨域访问,也是浏览器侧最大限度能想到的保护用户的方法
我们已经知道浏览器是禁止跨域的,在互联网时代,有很多场景是需要跨域的,比如前后台分离部署,数据共享,等等,不能因为安全,让业务受到阻碍,所以慢慢的就引入了第一个解决跨域的问题
首先,先了解一下html允许跨域的标签,禁止跨域是浏览器的杰作,想要跨域,当然要从前端做一些文章,我们经常看到某个网站引用另一个网站的图片啊、js代码等等,就是这些标签是不受跨域的限制
再来看list-json.php
// 连接数据库并访问数据
$conn = new mysqli('127.0.0.1', 'root', '123456', 'users') or die("数据库连接不成功.");
$conn->set_charset('utf8');
// $sql = "select articleid, author, viewcount, createtime from article where articleid<5";
$sql = "select aid, author, create_time from articles where aid<5";
$result = $conn->query($sql);
// 输出JSON数据到页面
$json = json_encode($result->fetch_all(MYSQLI_ASSOC));
//这里$_GET["callback"]自然就是传递过来的参数,callback=test,所以$_GET["callback"]就为test
//echo $_GET["callback"] . "(" . $json . ")";可以看成echo test. "(" . $json . ")";
//php中符号.连接字符串,自然为echo test($json);
//这是个啥,代码中什么时候会写成这样test($json)?当然是函数调用的时候,但是php里没有test函数啊,但是别忘了还有echo
//echo把test($json)作为响应返回给了前端
echo $_GET["callback"] . "(" . $json . ")";
$conn->close();
?>
//我们再回到前端,这时候等价于
//
//事先定义好了test函数,但是我并不用,当后台返回了test(json)的时候,我再调用,这就是JSONP的回调
虽然代码只有简单的几行,但是确实很绕,文字难以描述清晰,如果还是无法搞清楚,建议B站蜗牛学苑,里面有整套的课程,也共享了所有工具和代码
通过JSONP,我们是可以对JSON格式的数据进行跨域访问,也是通过script引入js,发送GET请求,那如果是POST或者其他类型的请求呢?
上述代码是最简单最能说明问题,也就是最有可能有漏洞,我们对用户的输入没有做任何的校验,如果用户的callback参数不传入test呢?
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>List-JSONPtitle>
<script src="http://192.168.174.134:82/list-json.php?callback=alert(document.cookie)//">script>
head>
<body>
body>
html>
callbak为alert(document.cookie)//,前面输入cookie,后面//注释掉没用的参数
当然这里引入一个cookie的httponly,这个属性是可以保护我们的cookie不被js代码获取,也是防御XSS较为有效的手段
每个服务器应该都有地方可以配置,我这里用的php,就在php.ini中配置
配置后js就拿不到了
另外,我们上面也说代码是最能说明问题的,也是最简单的,既然用户传入的参数不传test了,我们是不是应该暴力一点,跟需要跨域的人约好我们自己的关键字,并且长度等也要限制,例如
// 连接数据库并访问数据
$conn = new mysqli('127.0.0.1', 'root', '123456', 'users') or die("数据库连接不成功.");
$conn->set_charset('utf8');
// $sql = "select articleid, author, viewcount, createtime from article where articleid<5";
$sql = "select aid, author, create_time from articles where aid<5";
$result = $conn->query($sql);
// 输出JSON数据到页面
$json = json_encode($result->fetch_all(MYSQLI_ASSOC));
$callback = $_GET["callback"];
if (($callback !== "test") || strlen($callback) > 8) {
die("你在渗透我");
}
// echo $json;
echo $callback . "(" . $json . ")";
$conn->close();
?>
当我们了解了JSONP时,总会觉得不是那么智能,既然请求的时候有origin,响应的时候有Access-Control-Allow-Origin,把这个好好利用起来,代码不就不用改变了,origin是浏览器自主增加的,换句话说,你服务器就要确定好,到底都有谁访问你,你进行响应的时候全局加上Access-Control-Allow-Origin,写上白名单不就完事了
直接上代码cors.php
set_charset('utf8');
// $sql = "select articleid, author, viewcount, createtime from article where articleid<5";
$sql = "select aid, author, create_time from articles where aid<5";
$result = $conn->query($sql);
// 输出JSON数据到页面
$json = json_encode($result->fetch_all(MYSQLI_ASSOC));
// header("Access-Control-Allow-Origin: *");
// header("Access-Control-Allow-Origin: http://192.168.174.5");
//Origin白名单
$origin_list = array('http://192.168.174.1', 'http://192.168.174.5:82');
if (in_array($_SERVER['HTTP_ORIGIN'], $origin_list)) {
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
} else {
die("不允许的跨域");
}
echo $json;
$conn->close();
?>
当然,前端别忘了修改cors.html
Document
访问查看
查看php核心代码判断请求中origin内容在不在我数组中,在了就把请求中origin的值作为响应中Access-Control-Allow-Origin的值进行返回
//Origin白名单
$origin_list = array('http://192.168.174.1', 'http://192.168.174.5:82');
if (in_array($_SERVER['HTTP_ORIGIN'], $origin_list)) {
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
} else {
die("不允许的跨域");
}
所以,抓包如下
是不是简单而有效的防御手段呢?
当然Access-Control-Allow-Origin家族还有很多的Access-Control-Allow-某某某的,例如请求方法等,大家可以了解
其实也就是一个响应头叫做Content-Security-Policy,简称CSP
假设有一个留言板有存储型XSS,并且已经被我插入了自己的XSS代码
yesyesyes
当然xssrecv.php
set_charset("utf8");
$sql = "insert into xssdata(ipaddr, url, cookie, createtime) values('$ipaddr', '$url', '$cookie', now())";
$conn->query($sql) or die(mysqli_error($conn));
?>
那么,每当我访问,每次点击图片后会重定向到我的XSS简易平台,平台就把url和cookie写入到数据库中
这时,我对read.php开启csp,注意,csp常被用来禁止加载非本域的资源
但我插入的图片,连跳转都无法跳转
当然XSS平台也无法收到Cookie
开启白名单
似乎也不行
像这种单行的,带有location的似乎主要需要开启的是
开启后,就跳转了
当然也可以有效防御执行非本站的,比如我这里插入了bluelotus
正常是可以窃取的
再开启
又阻断了
csp还有个预警报告,就是可以将拦截的记录写入报告中
首先响应头改为
header("Content-Security-Policy: script-src 'self'; report-uri http://192.168.174.134:82/cspreport.php");
那134就得加入cspreport.php
$value) {
fwrite($file, $key . ": " . $value . "\r\n");
}
fwrite($file, "\r\n----------------------------\r\n\r\n");
fclose($file);
?>
万事俱备,再次访问
查看文件
SQL导入
/*
Navicat Premium Data Transfer
Source Server : 174.134-3306
Source Server Type : MySQL
Source Server Version : 100138
Source Host : 192.168.174.134:3306
Source Schema : users
Target Server Type : MySQL
Target Server Version : 100138
File Encoding : 65001
Date: 07/09/2022 09:06:16
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for articles
-- ----------------------------
DROP TABLE IF EXISTS `articles`;
CREATE TABLE `articles` (
`aid` int(4) NOT NULL AUTO_INCREMENT,
`subject` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`create_time` datetime(0) DEFAULT NULL,
`content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`author` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`is_delete` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`aid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 29 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of articles
-- ----------------------------
INSERT INTO `articles` VALUES (1, 'python', '2022-08-01 11:54:12', 'python is master', 'yuri', '1');
INSERT INTO `articles` VALUES (2, 'python', '2022-08-01 11:54:46', 'python is master', 'python is master', '1');
INSERT INTO `articles` VALUES (3, 'java', '2022-08-01 13:59:06', 'java', 'master', '1');
INSERT INTO `articles` VALUES (4, 'python', '2022-08-01 11:54:12', 'python is master', 'yuri', '1');
INSERT INTO `articles` VALUES (5, 'python', '2022-08-01 11:54:46', 'python is master', 'python is master', '0');
INSERT INTO `articles` VALUES (6, 'java', '2022-08-01 13:59:06', 'java', 'master', '0');
INSERT INTO `articles` VALUES (7, 'java', '2022-08-01 13:59:06', 'java', 'master', '0');
INSERT INTO `articles` VALUES (8, '1111', '2022-08-01 11:54:12', 'i love php and python', 'yuri', '1');
INSERT INTO `articles` VALUES (9, 'php and py', '2022-08-01 11:54:12', 'i love php and python', 'yuri', '0');
INSERT INTO `articles` VALUES (10, 'php and py', '2022-08-01 11:54:12', 'i love php and python', 'yuri', '0');
INSERT INTO `articles` VALUES (11, 'php and py', '2022-08-01 11:54:12', 'i love php and python', 'yuri', '0');
INSERT INTO `articles` VALUES (12, 'php and py', '2022-08-01 11:54:12', 'i love php and python', 'yuri', '0');
INSERT INTO `articles` VALUES (13, 'php and py', '2022-08-01 11:54:12', 'i love php and python', 'yuri', '1');
INSERT INTO `articles` VALUES (14, '123', '2022-08-15 16:12:51', '321', 'john', NULL);
INSERT INTO `articles` VALUES (15, '一万个舍不得', '2022-08-15 16:19:41', '我是永远\n\n爱', 'john', NULL);
INSERT INTO `articles` VALUES (16, '123', '2022-08-15 16:27:16', '123', 'john', '0');
INSERT INTO `articles` VALUES (17, '123', '2022-08-15 17:45:08', '这是一篇文章\n\n', 'john', '0');
INSERT INTO `articles` VALUES (18, 'gogogo', '2022-08-15 17:46:33', 'yes\n\n', 'john', '0');
INSERT INTO `articles` VALUES (19, 'gogogo', '2022-08-15 17:48:33', 'yesyesyes\n\n', 'john', '0');
INSERT INTO `articles` VALUES (20, 'gogogo1', '2022-08-15 17:51:35', 'gogogo\n', 'john', '0');
INSERT INTO `articles` VALUES (25, '888', '2022-08-15 17:57:40', '999\n', 'john', '0');
INSERT INTO `articles` VALUES (26, '111', '2022-08-15 17:59:04', '222\n\n333', 'john', '0');
INSERT INTO `articles` VALUES (27, '123', '2022-08-15 18:01:34', '123\n', 'john', NULL);
INSERT INTO `articles` VALUES (28, 'hahaha', '2022-08-16 10:32:27', 'xsss\n', 'john', NULL);
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`uid` int(15) NOT NULL AUTO_INCREMENT,
`username` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`password` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`role` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`head` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`createtime` datetime(0) DEFAULT NULL,
`failcount` tinyint(4) DEFAULT 0,
`lasttime` datetime(0) DEFAULT NULL,
PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'john', '123456', 'w', NULL, NULL, 0, '2022-09-06 11:50:37');
INSERT INTO `users` VALUES (2, 'tom', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (3, 'jerry', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (4, 'niko', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (5, 'wangdefa', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (6, 'john1', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (7, 'john2', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (8, 'john3', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (9, 'john4', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (10, 'john5', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (11, 'john6', '123456', 'r', NULL, NULL, 0, NULL);
INSERT INTO `users` VALUES (12, 'john10', '123456', 'r', '20220802_043705.jpg', '2022-08-02 04:37:05', 0, NULL);
INSERT INTO `users` VALUES (13, 'john11', '123456', 'r', '20220802_043909.jpg', '2022-08-02 04:39:09', 0, NULL);
INSERT INTO `users` VALUES (14, 'jenn', '123456', 'r', '20220817_091004.jpg', '2022-08-17 09:10:04', 0, NULL);
INSERT INTO `users` VALUES (15, 'jenn1', '123456', 'r', '20220817_091221.jpg', '2022-08-17 09:12:21', 0, NULL);
-- ----------------------------
-- Table structure for xmltable
-- ----------------------------
DROP TABLE IF EXISTS `xmltable`;
CREATE TABLE `xmltable` (
`testxml` text CHARACTER SET utf8 COLLATE utf8_general_ci
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of xmltable
-- ----------------------------
INSERT INTO `xmltable` VALUES ('\r\n 2021-08-20 03:03:53 \r\n XXX \r\n YYY \r\n ');
-- ----------------------------
-- Table structure for xssdata
-- ----------------------------
DROP TABLE IF EXISTS `xssdata`;
CREATE TABLE `xssdata` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ipaddr` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`cookie` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`createtime` datetime(0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of xssdata
-- ----------------------------
INSERT INTO `xssdata` VALUES (1, '192.168.174.1', 'http://192.168.174.134/security/read.php?id=27', 'PHPSESSID=2140fb0q79s0eopbmgf1kdqit4', '2022-08-15 18:01:38');
SET FOREIGN_KEY_CHECKS = 1;