关于前端:记一次-Github-项目依赖的安全警告修复-分析

54次阅读

共计 9517 个字符,预计需要花费 24 分钟才能阅读完成。

写这篇文章是感觉在解决问题的过程中 能够补全边缘常识 & 学习开源我的项目的办法。大家看看本人 Github 的我的项目如果有平安正告的话能够参考本文思路一起练练手~

提要

修复章节 别离讲述了如何通过 页面 & 命令行 形式(git rebase)解决平安正告

剖析章节 对 axios 本次版本升级(v0.21.0 => v0.21.1)所解决的次要问题进行剖析、并尝试用 TDD 的形式解决问题。

零、起因

筹备刷题筹备面试,进到 Github 刷题仓库 发现上面这个平安正告:

翻译一下就是说有 ” 潜在的安全漏洞 ”,关上来看看:

竟然是 axios 的破绽修复小版本升级,作为上周刚刚浏览过 0.21.0 版本 axios 源码的我当然是要理解一下的啦(而后水篇文章

一、修复

可能文章的程序有点奇怪,毕竟个别问题都是先 ” 剖析 ” 后 ” 修复 ” 的。不过我的项目依赖的安全漏洞修复办法千篇一律,并且前面的剖析过程一半以上可能只适宜前端方向的同学看,所以就把 ” 修复 ” 环节前置了~

说说修复办法吧,个别就是手动修复完依赖版本提交到主分支或者以后分支嘛(请依据本人我的项目的公布流程而确定批改分支)。不过当初 Github 检测到 CVE(Common Vulnerabilities & Exposures”通用破绽披露,本次是 CVE-2020-28168)后个别会基于主分支创立一个修复分支.

既然官网曾经帮咱们改完了那就没必要本人搞了,间接 git rebase 一波杀穿(主分支做了提交限度 or 流程存在门禁就的就老老实实 merge 吧),能够通过网页或者命令行的形式来解决问题:

P.S. 就算是我的分支之前刚做了提交也没问题,如下图所示

1. 网页操作形式

Tip:如果对命令行的操作没有 200% 的信念真的真的请抉择这个,标准易操作出错也有提醒真心难受

如果想在网页上解决的话就进入到我的项目的 Pull requests 标签页,确认无误之后抉择合并形式:

这里我抉择的是 Rebase adn merge,它会作为一次最新的提交合并入代码中(而且还能够间接删除掉 Github 主动生成平安正告的分支,方便快捷),后果如下:

2. 命令行形式

git fetch # 拉我的项目最新信息
git pull origin master # 确保主分支最新
git checkout -b tmp origin/dependabot/npm_and_yarn/axios-0.21.1 # 把 "Github 主动修复分支" 拉到本地 tmp 分支
git rebase master tmp # 变基操作:在 tmp 分支上执行,将 tmp 的更新内容尾接到 master 上,后果存储于 tmp 分支
git push origin tmp:master # 将 tmp 作为 master , 提交到近程仓库的 master 分支上
git push origin --delete dependabot/npm_and_yarn/axios-0.21.1  # 手动删除 "Github 主动修复分支" 

实现之后后果与网页形式是统一的,实现之后再进入到仓库页面,发现安全漏洞正告提醒曾经隐没了(撒花✿✿ヽ (°▽°) ノ✿

二、剖析

下面的 Git 操作教学 修复局部 临时就告一段落了,当初回过头来看看 axios 到底是出了什么问题导致了平安正告,咱们进入到 Pull request 标签页:

Release notes 展现了 v0.21.1 绝对于 v0.21.0 产生的更新,其中 Internal and Tests 局部 都是测试用例的修复,与平安正告关系不大,所以让咱们把重点放在 Fixes and Functionality 局部,第一项 Hotfix: Prevent SSRF 就是对于平安正告的信息。

从前面的链接 #3410 点进去,就能理解到这个修复的起因、探讨过程和解决办法。接下来咱们就从最初始的 issue 来剖析这个平安正告。

0. 前置常识:什么是「追随重定向(Follow Redirects)」

Tip:原本应该间接开始剖析 issue 的,然而这个知识点可能会影响到对 issue 的剖析浏览,所以就前置到这里来了

我先在本地 8080 启动一个返回 302 的服务器程序,大家脑补一下通过 浏览器Node.JS 的内置 http 模块Node.JS 环境下的 axios 拜访 localhost:8080 链接别离会失去什么后果,程序如下:

const axios = require('axios')
const http = require('http')

http.createServer(function (req, res) {res.writeHead(302, {location: 'http://example.com'})
    res.end()}).listen(8080) 

当初揭晓答案:

  • 浏览器:进入重定向提供的指标网站 http://example.com

基于 MDN 的说法:

302:该状态码示意所申请的 URI 资源门路长期扭转, 并且还可能持续扭转. 因而客户端在当前拜访时还得持续应用该 URI. 新的 URL 会在响应的 Location: 头字段里找到.

加上察看网络申请的过程就能推导出 浏览器 的申请过程如下图所示,其中 ” 申请过程 2″ 就是 浏览器主动实现了「追随重定向(Follow Redirects)」

  • Node.JS 的内置 http 模块:失去 302 状态码,阐明内置的 http 模块不具备 「追随重定向 (Follow Redirects)」 能力,只是执行了第一个申请过程,也就是获取到 302 状态码及重定向地址即可:

  • Node.JS 环境下的 axios:和 浏览器 的最终后果一样,状态码 200 并且可能获取到网页内容

那么为什么同样是 Node.JS 环境下 axios 内置 HTTP 模块 对于重定向体现不同呢?在 axios Github 文档 中搜寻 redirects,能够再 申请配置 大节找到这样的信息:

意思是 axios 默认开启了 「追随重定向(Follow Redirects)」 能力(顺便考古到了 2015 年是怎么给 axios 加上这个性能的 follow redirects),并且可能通过扭转 maxRedirects 字段的值来决定 「追随重定向 (Follow Redirects)」 的开启或敞开。遇到 302 状态码时,如果开启了 「追随重定向(Follow Redirects)」 就会获取重定向地址并持续跳转,如果不开启就是间接返回后果.

当初把 maxRedirects 字段置为 0,再次运行程序,能够看到 axios 对于重定向的解决体现就和 NodeJS 统一了:

理解什么是 「追随重定向(Follow Redirects)」 之后,咱们来开始看看提 issue 的老哥遇到的问题~

1. 初始 issue 剖析

先看看这个平安正告的起因:

  • 链接:issue3369 – 重定向后的申请未通过代理传递
  • 阐明:提 issue 的老哥提出他应用 “ 携带代理配置 ” 的 axios 进行拜访,因为申请通过代理服务器,且代理服务器永远返回 302,所以老哥期待的运行景象是:

    • 1)首次申请因为配置了带来,故通过代理服务器地址 localhost:8080,取得 302 状态码,因为 axios 默认开启 「追随重定向(Follow Redirects)」 所以临时不打印后果,并且 axios 会主动尝试拜访重定向后的指标地址 http://example.com;
    • 2)然而因为代理配置的存在,第二次拜访还是会拜访到 localhost:8080,再次取得 302 状态码;
    • 3)反复下面两个步骤直到达到 axios 的 Follow Redirects 模式次数下限,而后报错;

那么咱们当初来看看提 issue 的老哥提供的残缺重现代码:

const axios = require('axios')
const http = require('http')

const PROXY_PORT = 8080
let count = 0

// A fake proxy server
http.createServer(function (req, res) {
      count++ //(我退出的计数逻辑)累加申请次数
    res.writeHead(302, {location: 'http://example.com'})
    res.end()}).listen(PROXY_PORT)

// 和咱们的前置案例相比新增了一个 "携带代理配置的 axios 申请"
axios({
  method: "get",
  url: "http://www.google.com/", // 轻易写个链接,都会被代理对象取代
  proxy: { // 重点局部
    host: "localhost",
    port: PROXY_PORT,
  },
})
.then((r) => {console.log(count)   // (我退出的打印) 打印胜利状况拜访代理服务器次数
  console.log(r.status) // (我退出的打印) 用于察看返回状态码
  console.log(r.data)
})
.catch(e => {console.log(count) // 打印失败状况拜访代理服务器次数
  console.error(e)
}) 

与咱们冀望的失去 302 报错后果不同,应用 axios v0.21.0 的执行成果如下:

景象为:进入代理服务器一次,而后 axios 的申请后果状态码是 200,内容是 http://example.com 的页面内容.

依据景象推导执行过程为:首次申请胜利进入了代理服务器并且累加了一次计数(且只有这一次),然而步骤 2 申请却绕过了 axios 配置里的代理 proxy 间接拜访了重定向地址 http://example.com.

那么当初问题就很显著了,当取得 302 返回时应该 携带代理配置 从新发动申请,然而 axios v0.21.0 在从新发动申请时却失落了 代理配置 ,所以要做的事件就是钻研下 axios 「追随重定向(Follow Redirects)」 之后为何失落了 代理配置.

2. 问题定位 & 修复计划制订

呈现问题的是 NodeJS 环境,那么天然要找到 axios 源码中的 NodeJS 重定向申请配置的地位。

来到 axios 我的项目的 /lib/default.js 地位,上面的代码赋予了 axios 实现 跨平台网络申请能力,它会主动判断运行平台并应用不同平台逻辑实现网络申请:

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {adapter = require('./adapters/xhr'); // 浏览器环境走这里
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {adapter = require('./adapters/http'); // Node.JS 环境走这里
  }
  return adapter;
} 

接下来进入到 /lib/adapters/http.js 后发现只有一个函数,接管 config 参数并返回一个 Promise:

申请逻辑实现应该都在这个函数外面了,接着间接搜寻 config.proxy 查找 代理逻辑,找到相干代码并失去如下剖析:

这里的 171 行的 httpFollow 来自 follow-redirects 模块,模块官网的形容是:

Drop-in replacement for Node’s http and https modules that automatically follows redirects.

也就是说该模块兼具内置 http & https 模块的能力,且还具备了 「追随重定向(Follow Redirects)」 能力(默认 「追随重定向(Follow Redirects)」 下限为 21 次,即 maxRedirects 属性)。

理解了这些之后,想要修复 “ 代理配置失落 ” 的问题,那么就要去理解 follow-redirects 模块的应用办法了,这里找到官网 demo:

const url = require('url');
const {http, https} = require('follow-redirects');

const options = url.parse('http://bit.ly/900913');
options.maxRedirects = 10;
options.beforeRedirect = (options, { headers}) => {
  // 重定向时调整 options
  if (options.hostname === "example.com") {options.auth = "user:password";}
};
http.request(options); 

那么这个 options.beforeRedirect 就是咱们要找的货色了,它在执行申请前传入 options,运行函数体实现对 options 的批改,所以须要 axios 我的项目的 /lib/adapters/http.js 中增加 beforeRedirect,在重定向的时候将本来的 代理配置 退出 options 即可.

4. 以 TDD 的形式修复 issue

为了测试修复成果,咱们将 axios v0.21.0 版本的源码 (commit 为 94ca24b5b23f343769a15f325693246e07c177d2) 拉到本地,并复制 axios v0.21.1 版本的测试用例 /test/unit/regression/SNYK-JS-AXIOS-1038255.js 的代码,到我的项目中新建同名文件夹并黏贴,上面是该测试用例的代码及剖析:

// https://snyk.io/vuln/SNYK-JS-AXIOS-1038255
// https://github.com/axios/axios/issues/3407
// https://github.com/axios/axios/issues/3369

const axios = require('../../../index');
const http = require('http');
const assert = require('assert');

const PROXY_PORT = 4777; // 代理服务器端口
const EVIL_PORT = 4666; // 重定向 location 地址的端口,代码逻辑正确的话不应该进入该端口

describe('Server-Side Request Forgery (SSRF)', () => {
  let fail = false;
  let proxy;
  let server;
  let location;
  beforeEach(() => {server = http.createServer(function (req, res) {
      fail = true;
      res.end('rm -rf /');
    }).listen(EVIL_PORT);
    proxy = http.createServer(function (req, res) {
      // 第一次申请达到代理服务器时,req.url 为 http://www.google.com/,走返回 302 的逻辑
      // 第二次申请由 axios 的「追随重定向(Follow Redirects)」能力收回,url 应为 http://localhost:4666
      if (req.url === 'http://localhost:' + EVIL_PORT + '/') {
        return res.end(JSON.stringify({
          msg: 'Protected',
          headers: req.headers, // 返回申请头
        }));
      }
      res.writeHead(302, { location}) // 第一次申请达返回状态码 302 和 http://localhost:4666
      res.end()}).listen(PROXY_PORT);
  });
  afterEach(() => {server.close();
    proxy.close();});
  it('obeys proxy settings when following redirects', async () => {
    location = 'http://localhost:' + EVIL_PORT;
    let response = await axios({
      method: "get",
      url: "http://www.google.com/",
      proxy: {
        host: "localhost",
        port: PROXY_PORT,
        auth: {
          username: 'sam',
          password: 'password',
        }
      },
    });

    assert.strictEqual(fail, false);
    assert.strictEqual(response.data.msg, 'Protected');
    assert.strictEqual(response.data.headers.host, 'localhost:' + EVIL_PORT);
    assert.strictEqual(response.data.headers['proxy-authorization'], 'Basic' + Buffer.from('sam:password').toString('base64'));

    return response;
  });
}); 

而后在本地用 npm test 或者 yarn test 跑测试,后果如下:

的确是新的测试用例炸了,因为 axios 中并没有实现相应能力所以没有提醒任何问题,当初咱们来验证编写修复代码:

问题出在 代理配置 失落,所以到 /lib/adapters/http.js 找到代理相干代码(148~152 行):

复制一份到 beforeRedirect 中(axios 我的项目有 lint 查看所以改 options 为 tmpOptions)这样应该就不会呈现代理失落的状况了~

if (proxy) {options.beforeRedirect = function(tmpOption) {
    tmpOption.hostname = proxy.host;
    tmpOption.host = proxy.host;
    tmpOption.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
    tmpOption.port = proxy.port;
    tmpOption.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;
  };
} 

将下面代码退出到 /lib/adapters/http.js 的 160 行处,再次用 npm test 或者 yarn test 跑测试,这次胜利地进行「追随重定向(Follow Redirects)」 申请并且因为超过最大次数而报错了:

之所以没有达到想要的成果,是因为没有为「追随重定向(Follow Redirects)」 申请配置正确的申请指标链接。本来测试用例冀望:

  • 第一次申请达到代理服务器时,req.url 为 http://www.google.com/,走返回 302 的逻辑并将「追随重定向(Follow Redirects)」 的指标链接设置为 http://localhost:4666
  • 第二次申请由 axios 的「追随重定向(Follow Redirects)」能力收回,req.url 应为 http://localhost:4666

然而理论状况是第一次申请取得 302 返回后并没有更新指标链接,所以还是要浏览下 follow-redirects 模块 中 options.beforeRedirect 的调用地位(根目录下的 index.js):

在 357 行能够看到调用 options.beforeRedirect 时传入了 options & 蕴含重定向响应体的 responseDetails,于是咱们从 response 中获取从新获取指标配置并填充 path 和 headers.host:

if (proxy) {options.beforeRedirect = function(tmpOption, response) { // response 对应重定向响应体的 responseDetails
    // hostname(host) & port 是申请发送到的服务器的域名 & 端口,当初都是代理配置不变
    // path & headers.host 是指标门路 & 指标 host,遇到 302 时应该读 location 而后从新填充
    tmpOption.hostname = proxy.host;
    tmpOption.host = proxy.host;
    tmpOption.port = proxy.port;
    // tmpOption.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
    // tmpOption.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;
    var redirectInfo = url.parse(response.headers.location);
    tmpOption.path = redirectInfo.href; // 重定向链接
    tmpOption.headers.host = redirectInfo.host; // 重定向的指标 host
  };
} 

再次运行测试,胜利通过:

5. 比照官网的问题解决办法

问题曾经处理完毕并且通过测试用例,然而可能存在疏漏所以肯定要与官网的修复进行比照验证,这样才合乎学习闭环。

能够看到 #3410 相干的提交:

这也是一个 TDD 的过程,首先是编写测试用例重现了 issue,而后对问题进行修复,而后再将代码重构。

当然也能够间接点击最初 File changed 的选项卡,间接看整体批改了哪些代码。

能够看到官网的解决办法除了重构局部之外与咱们下面的修复办法基本一致,不过其中有一个点引起了我的趣味:

options.beforeRedirect 办法体中竟然只用一个 redirection 就实现了 redirection.header.host 的赋值,而咱们下面是用到了第二个参数的,这里我抉择持续到 follow-redirects 模块 中找找,果然在 RedirectableRequest.prototype._processResponse 中找到了这段逻辑:

于是刚刚的修复代码能够不再应用第二个参数 response,并且也不必再从新解析一次 location 了:

if (proxy) {options.beforeRedirect = function(tmpOption) {
    var redirectHost = tmpOption.host; // 先拿进去,避免被笼罩
    tmpOption.hostname = proxy.host;
    tmpOption.host = proxy.host;
    tmpOption.port = proxy.port;
    tmpOption.path = tmpOption.href; // 重定向的指标门路
    tmpOption.headers.host = redirectHost; // 重定向的指标 host
  };
} 

再次运行测试,胜利通过:

OK 到这里对于 #3410 的剖析就全副实现了~ 对第二个问题 Protocol not parsed when setting proxy config from env vars #3070 也能够尝试用这样的办法解析学习哟~

撒花✿✿ヽ (°▽°) ノ✿

欢送拍砖,感觉还行也欢送点赞珍藏~
新开公号:「无梦的冒险谭」欢送关注(搜寻 Nodreame 也能够~)
旅程正在持续 ✿✿ヽ (°▽°) ノ✿

正文完
 0