企业网站CDN被刷流量怎么办

前一段公司国内几个网站的CDN流量被刷的厉害,解决问题之后一直忙于其他事情,没有时间分享,今天分析一下这个问题。

一、情况描述

公司在百度和阿里云上都开CDN的账户,大概是7月份,前后差不了几天,分别收到了百度和阿里云的短信通知,账户欠费,CDN被停用了,续费买了流量包,第二天又消耗完了,更可气的是阿里云竟然倒欠了大几百费用。要知道公司几个官网,平时一个TB流量能用大半年,出现这种情况肯定是被攻击了。

CDN流量消耗图

CDN流量消耗图


二、情况分析

分别进入百度和阿里云后台,可以看到流量攻击主要集中在晚八点到凌晨3点这个时间段。各种对图片,视频的请求纷至沓来,一个小时十几个G的流量就消耗殆尽了,这些情况有些是一个IP并发连续不停请求,有些是不同IP请求相同资源,所以尝试在CDN后台的配置根本无法抵抗这么大强度的攻击。
网络上出现了很多CDN流量被盗刷的抱怨,关于此类攻击的原因,网络上有很多说法,一些说传统CDN厂家对云服务商的攻击,还有一些说运营商跨省流量结算,一些地方需要下行流量平衡账单,等等,反正这种行为挺令人头疼。

三、解决方案

第一选项是上防火墙,阿里云WAF一年是8000左右,百度WAF按月付费最低700一月,这个选项直接排除,要知道我们服务器成本才99一年(阿里云活动),这个成本虽然不多,但不是长久之计。
第二选项是所有请求全部限速,IP,User-agent白名单,等等。很明显这无法满足要求,要么限速过低,正常访客浏览受限,要么限速过高,无法抵抗攻击。
第三选项安全圈内大佬提醒,使用第三方或自行开发类似cloudflare的“挑战-应答”模型,这是一种很好的过滤非法访问的方法,但要么占用服务器资源,要么可能会造成爬虫访问困难,进而影响网站在搜索引擎上的排名,或者影响网站访问速度,影响用户体验,最终也是没有采用。

找到一种低成本,高效率的方法过滤这些攻击,是理想的方法,通过分析上述的攻击情况,其实可以发现一些规律。
1,对于企业网站,HTML代码的体积是很小的,一般都是200KB以下,而图片视频等媒体资源,动则几MB,甚至几百MB,所以一些刷流量的请求通常不断访问媒体资源。
2,这些蹩脚的攻击,也会进行一些低成本的伪装,例如更换IP,伪造User-agent和Referer等,攻击也是需要成本的,越复杂的攻击,需要的投入越多。
3,正常的请求,一般都会先访问HTML或者JSON,然后才会访问CSS,JS,图片,视频等资源。

有了这个就好办了,我们可以这样设计一个控制模式:
对所有非HTML和JSON的请求,设置一个较低的默认访问带宽(2KB),同时限制单IP并发,这样可以把非法访问限制在一个消耗范围内,限制无法连接可能让这些攻击者去研究其他方式。
对于HTML和JSON请求,设置一个可正常浏览的速度(200KB),也限制单IP并发,并对没有附带安全COOKIE的请求,下发一个安全COOKIE,可以以用户的IP+安全字符作为令牌。
对于非HTML和JSON的请求,判断是否附带和附带的安全COOKIE是否正确,如果正确,设置一个较高的传送速度(一般800KB/s足够),对于未附带或者附带的安全COOKIE不正确的请求,设置一个低速(第一条说的2KB),不至于让攻击者发现。
对于搜索引擎爬虫,单独判断并设置一个传送速度。
使用CDN的边缘脚本执行,完全独立于网站服务器,不用对网站做任何更改变动。

最终,我开发部署了如下脚本:
百度云:

var crypto = require('crypto');
var gethash = (str)=> {
    var hmac = crypto.createHmac('sha256', 'Happy birthday');
    return hmac.update(str).digest('hex');
};
var getcookie = (name)=> {
    var cookies = r.headersIn.Cookie ? r.headersIn.Cookie.split(';') : [];
    for (var i = 0; i < cookies.length; i++) {
        var cookie = cookies[i].split('=');
        if (cookie && cookie[0].trim() === name) {
            return cookie[1].trim();
        }
    }
    return null;
};
var setcookie = (name, value)=> {
    var d = new Date();
    d.setTime(d.getTime() + 3600000);
    var expires = d.toUTCString();
    r.headersOut['Set-Cookie'] = `${name}=${value}; expires=${expires}; Max-Age=3600; path=/; domain=example.com; SameSite=Lax; secure; HTTPOnly`;
};
var getContentType = ()=> {
    var mimetype = r.headersOut['Content-Type'].split(';');
    if(mimetype)
        return mimetype[0].trim();
    return mimetype;
};
var is_search_bot = ()=> {
    var spiders = ['Googlebot', 'AdsBot-Google', 'Bingbot', 'AdIdxBot', 'BingPreview', 'MicrosoftPreview', 'Baiduspider', 'YandexBot', 'Applebot', 'DuckDuckBot', 'Sogou Pic Spider', 'Sogou head spider', 'Sogou web spider', 'Sogou Orion spider', 'facebookexternalhit', 'Slurp', '360Spider', 'YisouSpider'];
    var ua = r.headersIn['User-Agent'];
    for (var i = 0; i < spiders.length; i++) {
        if (ua.includes(spiders[i])) 
            return true;
    }
    return false;
};

var get_protected_speed = ()=> {
    var mimetype = getContentType();
    var uri = r.uri;
    var referer = r.headersIn['Referer'];
    var hash = gethash(r.remoteAddress);

    if ( mimetype != 'text/html' && uri == referer )
        return '1k';


    if ( is_search_bot() )
        return '100k';

    if ( mimetype.includes('video/') && getcookie('captchaprotect') == hash )
        return '800k';

    if ( getcookie('captchaprotect') == hash )
        return '500k';

    if ( mimetype == 'text/html' || mimetype == 'application/json' )
        return '200k';

    return '2k';
}
var modify_header = ()=> {
    var mimetype = getContentType();
    var hash = gethash(r.remoteAddress);
    if( ( mimetype == 'text/html' || mimetype == 'application/json') && getcookie('captchaprotect') != hash )
        setcookie('captchaprotect', hash);
   
    var speed = get_protected_speed();
    r.headersOut['X-Cache-BD'] = speed;
    r.variables.limit_rate =  speed;
};

r.respHeader(modify_header);

阿里云

reqhost = req_host()
requri = req_uri()
referer = req_referer()
cookie = req_cookie('captchaprotect')
hash = tohex(hmac('Happy birthday', $remote_addr, 'sha256'))

def setcookie (name, value) {
    expires = cookie_time(add(time(), 3600))
    add_rsp_cookie(name, value, ['expires' = expires, 'max_age' = 3600, 'domain' = reqhost, 'path' = '/', 'samesite' = 'lax', 'secure' = true, 'httponly' = true])
}

def is_resource () {
    suffixs = ['.rar', '.zip', '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.pdf', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tif', '.gif', '.psd', '.svg', '.mp4', '.m4p', '.mp3', '.mpg', '.mpeg', '.m4v', '.wmv', '.mov', '.qt', '.avi', '.flv', '.rmvb', '.rm', '.3gp', '.webm', '.mkv', '.vob', '.m3u8', '.ts', '.mts', '.m2ts', '.ogg', '.eot', '.otf', '.woff', '.woff2', '.ttf', '.txt', '.css', '.js']

    for i, suff in suffixs {
        start = sub(0, len(suff))
        target = lower(substr(requri, start, -1))
        if eq (target, suff) {
            return true
        }
    }
    return false
}

def is_video () {
    suffixs = ['.mp4', '.m4p', '.mpg', '.mpeg', '.m4v', '.wmv', '.mov', '.qt', '.avi', '.flv', '.rmvb', '.rm', '.3gp', '.webm', '.mkv', '.vob', '.m3u8', '.ts', '.mts', '.m2ts', '.ogg']

    for i, suff in suffixs {
        start = sub(0, len(suff))
        target = lower(substr(requri, start, -1))
        if eq (target, suff) {
            return true
        }
    }
    return false

}

def is_search_bot () {
    spiders = ['Googlebot', 'AdsBot-Google', 'Bingbot', 'AdIdxBot', 'BingPreview', 'MicrosoftPreview', 'Baiduspider', 'YandexBot', 'Applebot', 'DuckDuckBot', 'Sogou Pic Spider', 'Sogou head spider', 'Sogou web spider', 'Sogou Orion spider', 'facebookexternalhit', 'Slurp', '360Spider', 'YisouSpider']
    for i, bot in spiders {
        if req_user_agent(bot) {
            return true
        }
    }
    return false
}

def get_protected_speed () {

    if and ( eq (is_resource(), true), eq (requri, referer) ) {
        return  1
    }

    if eq (is_search_bot(), true) {
        return 100
    }

    if and ( eq ( is_video(), true),  eq ( hash, cookie) ) {
        return 800
    }

    if eq ( hash, cookie ) {
        return 500
    }

    if eq (is_resource(), false) {
        return 200
    }

    return 2
}

if and ( eq (is_resource(), false), ne(cookie, hash) ) {
    setcookie('captchaprotect', hash)
}

speed = get_protected_speed()

add_rsp_header('X-Cache-BD', tostring(speed))
limit_rate_after(0, 'k')
limit_rate(speed, 'k')

在调试的过程中,发现了一些问题
1,百度CDN扩展支持较强,能完全实现上述设想
2,阿里云CDN无法实现全部设想,一是最小限速最低是50KB,二是它的边缘脚本有一个执行位置选择,总共支持三个,但后台只支持两个,需要发工单人工更改到可以限速的执行位置,但这个执行位置无法获取返回的MIMETYPE,只能通过请求的资源后缀判断,可能不准确。

在CDN上部署上述脚本之后,目前整个流量消耗已经平稳,阿里云虽然没有完全实现,但也抵御了很多攻击。整个方法虽然并不无懈可击,但目前解决问题,后续可以根据情况变化进行升级。

阅读资料:
阿里云CDN可能因为盗刷流量出现高额账单(超过充值金额很多): https://www.alibabacloud.com/help/zh/cdn/product-overview/configure-high-bill-alerts
如何避免CDN被恶意攻击和恶意刷流量: https://help.aliyun.com/document_detail/362059.html

发表回复