浅谈跨域安全

简介

好好学习,天天向上

跨域问题对于我本人来说一直是一个很头疼的问题,因为每次听到什么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格式的数据

浅谈跨域安全_第1张图片

紧接着,我在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>

当然没问题,自己读自己,同一个域当然可以访问

浅谈跨域安全_第2张图片

那么这次我们在另一台192.168.174.5上发布这个html去读取192.168.174.134的数据呢,这样是否可以读取到呢?注意最关键的URL

浅谈跨域安全_第3张图片

访问一下试试看,界面空白无所谓,也没有弹窗,只能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')。

浅谈跨域安全_第4张图片

其实也很明显,上面第一点我们才说到”三元组“,其中IP变了,自然就跨域了,既然跨域了,第二点浏览器报出的异常,是浏览器做了拦截,为了验证这一点,我们拿出fiddler先访问不涉及跨域的

http://192.168.174.134:82/list-json.html

可以看到两个请求,先访问的html,html再去请求php,当然第二个请求也返回了数据

浅谈跨域安全_第5张图片

浅谈跨域安全_第6张图片

我们再次访问涉及跨域的

http://192.168.174.5:82/list-json.html

可以看到第一个请求没有什么区别

浅谈跨域安全_第7张图片

重点就在第二个请求上,第一个请求我们正常访问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域,没有允许就报错

浅谈跨域安全_第8张图片

相信到了这一步,大家对跨域都有了了解

那么,我现在再换台电脑换另个IP,使用python直接往http://192.168.174.134:82/list-json.php发送请求获取这个数据,是否可以正常访问呢?
应该不会有人说否吧?
跨域是浏览器的安全机制,我用python发请求,根本不会经过浏览器,所以压根就没有origin的事儿

答案当然是可以访问,所以一定要牢记最开始说的两点

这个时候可能会有疑问,那用户都像我们这样用python/php写个脚本,或着自己抓包修改把请求响应里面改一下不就能访问了,当然能访问了,但是但凡一个正常的用户99%不会像安全人员一样,在输入框输入select的,正常用户都是在浏览器上进行操作,压根不会考虑说我抓个包改一改,或者直接给后台发请求,所以禁止跨域访问,也是浏览器侧最大限度能想到的保护用户的方法

浅谈跨域安全_第9张图片

JSONP

JSONP回调

我们已经知道浏览器是禁止跨域的,在互联网时代,有很多场景是需要跨域的,比如前后台分离部署,数据共享,等等,不能因为安全,让业务受到阻碍,所以慢慢的就引入了第一个解决跨域的问题

首先,先了解一下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劣势

请求方式

通过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,后面//注释掉没用的参数
    

浅谈跨域安全_第12张图片

当然这里引入一个cookie的httponly,这个属性是可以保护我们的cookie不被js代码获取,也是防御XSS较为有效的手段

每个服务器应该都有地方可以配置,我这里用的php,就在php.ini中配置

浅谈跨域安全_第13张图片

配置后js就拿不到了

浅谈跨域安全_第14张图片

另外,我们上面也说代码是最能说明问题的,也是最简单的,既然用户传入的参数不传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();

?>

浅谈跨域安全_第15张图片

浅谈跨域安全_第16张图片

CORS

当我们了解了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
    


    


访问查看

浅谈跨域安全_第17张图片

查看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("不允许的跨域");
}

所以,抓包如下

浅谈跨域安全_第18张图片

是不是简单而有效的防御手段呢?

当然Access-Control-Allow-Origin家族还有很多的Access-Control-Allow-某某某的,例如请求方法等,大家可以了解

CSP

其实也就是一个响应头叫做Content-Security-Policy,简称CSP

假设有一个留言板有存储型XSS,并且已经被我插入了自己的XSS代码

yesyesyes


浅谈跨域安全_第19张图片

当然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写入到数据库中

浅谈跨域安全_第20张图片

浅谈跨域安全_第21张图片

浅谈跨域安全_第22张图片

这时,我对read.php开启csp,注意,csp常被用来禁止加载非本域的资源

浅谈跨域安全_第23张图片

但我插入的图片,连跳转都无法跳转

浅谈跨域安全_第24张图片

当然XSS平台也无法收到Cookie

开启白名单

浅谈跨域安全_第25张图片

似乎也不行

浅谈跨域安全_第26张图片

像这种单行的,带有location的似乎主要需要开启的是

浅谈跨域安全_第27张图片

开启后,就跳转了

浅谈跨域安全_第28张图片

当然也可以有效防御执行非本站的,比如我这里插入了bluelotus

浅谈跨域安全_第29张图片

正常是可以窃取的

浅谈跨域安全_第30张图片

再开启

浅谈跨域安全_第31张图片

又阻断了

浅谈跨域安全_第32张图片

csp还有个预警报告,就是可以将拦截的记录写入报告中

首先响应头改为

header("Content-Security-Policy: script-src 'self'; report-uri http://192.168.174.134:82/cspreport.php");

浅谈跨域安全_第33张图片

那134就得加入cspreport.php

 $value) {
    fwrite($file, $key . ": " . $value . "\r\n");
}
fwrite($file, "\r\n----------------------------\r\n\r\n");
fclose($file);

?>

万事俱备,再次访问

浅谈跨域安全_第34张图片

查看文件

浅谈跨域安全_第35张图片

附录

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;

你可能感兴趣的