muspi_merol / blog / suac126h4dw6dizd

最后更新于:4月3日

一种DDOS的应对方式


最近网站遭到了DDOS,流量飙得老高,100GB的流量包半周就能耗完。发现攻击集中在 376 KB 的最大的bundle文件上。由于攻击来自多个IP,且(由于我比较菜)没有什么特征可以分辨,逐url逐IP的令牌桶没效果,所以一直都没有实质性的办法。

说起来,这人一开始还通过尾随 queryString 的方式击穿我CDN的缓存,跑了我源站几十GB的流量直接给我干欠费了 哈哈哈

今天突然想到了一种办法通过cookies的方式,让浏览器访问的友好用户可以正常访问,而恶意攻击不会占用我太大流量:

我先是创建了一个自动刷新的HTML:

<html>

<head>
    <meta http-equiv="refresh" content="0">
</head>

</html>

还可以更短:

<meta http-equiv="refresh" content="0">

我的思路是,让所有请求都解析出这个HTML,但响应投中包含一个set-cookies,刷新后的请求发现cookies对上了就返回原始数据。这个方案的优势在于:

  1. 可以完全作为中间件存在,无需改动服务源码
  2. 用户无感,因为这个刷新实测非常快

事实上,这其实很容易被破解,攻击者只需要模仿浏览器执行set-cookies的行为即可。如果他确实这么做了,那我觉得只能把set-cookies放在<script>中实现,然后结合混淆的方式来改进了。毕竟要是要执行js的话,基本上就得用Puppeteer、Selinium之类的麻烦玩意儿了

欢迎大家开无痕打开Devtools去尝试尝试我部署了这个中间件的网页

效果

附录

源码如下:

import aiohttp
from sanic import Request, Sanic
from sanic import __version__ as sanic_version
from sanic.response import empty, file, raw

app = Sanic(__name__)

@app.route("/<path:path>")
async def index(request, path):
    async with aiohttp.ClientSession() as session:
        async with session.get(
            f"<protocol>://<host>:<port>/{path}", headers=request.headers
        ) as resp:
            headers = resp.headers.copy()
            headers.pop("Transfer-Encoding", None)
            headers.pop("Content-Encoding", None)

            return raw(await resp.read(), status=resp.status, headers=headers)

CHECK_KEY = "human"
CHECK_VAL = "Hi!"

@app.on_request
async def check_not_first(request: Request):
    if CHECK_KEY in request.cookies:
        if request.cookies.get(CHECK_KEY) == CHECK_VAL:
            return

    if "text/html" in request.headers.get("accept", ""):
        response = await file("./redirect.html")
        response.add_cookie(CHECK_KEY, CHECK_VAL)
        return response
    else:
        return empty()