13

手把手教你自定义实现一个npm audit

 3 years ago
source link: https://www.freebuf.com/sectool/247889.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

npm audit命令可以帮助检测项目的依赖包是否存在已知的漏洞,漏洞库来源: Security advisories 。 当希望将依赖组件漏洞纳入SAST漏洞扫描范围是,通常的想法是通过执行npm audit命令以获取相关的结果。

const run = () =>{
  const auditCommand = 'npm audit --registry=https://r.cnpmjs.org/ --audit-level=high --production --json';
  const execOptions = { maxBuffer: 10 * 1024 * 1024 };

  exec(auditCommand, execOptions, function (error, stdout, stderr) {
      if (error !== null) {
      console.log('exec error: ' + error);
        return;
      }

      if (stdout) {
        console.log(stdout);
      }
  });
}

但是执行的时候往往或出现下面的错误。

exec error: Error: Command failed: npm audit --registry=https://r.cnpmjs.org/ --audit-level=high --production --json

而且使用npm audit不能控制依赖的深度,返回的结果为所有深度的依赖项目。

2.解决问题

如果想要自定义实现一套自己的npm audit,需要解决哪些问题呢?我觉得有如下几个问题需要解决:

  • 如何获取漏洞库?
  • 从package.json中解析一级依赖。
  • 根据package-lock.json解析并生成依赖树。
  • 从依赖树中生成依赖链。
  • 判断当前引用版本是否存在问题。

2.1 漏洞库获取

同npm audit一样,我们使用 Security advisoriesd 的漏洞库,该漏洞库可以直接通过相关的接口获取,只是在header头中需要设置“'x-spiferack': '1'”,这里不再赘述,部分代码如下。

const result = await axios.get(base_url + '?page=' + pageNum.toString() + '&perPage=' + perPageNum.toString(),
      {
        headers: {
            'x-requested-with': 'XMLHttpRequest',
            'x-spiferack': '1',
        },
      });
      const data_json = result.data;
      total_num = data_json.advisoriesData.total;
      const objects = data_json.advisoriesData.objects;

2.2 package.json解析

由于package.json是一个json文件,可以直接读取文件内容,然后通过JSON.parse()方法获取相关的json数据,并从dependencies,devDependencies中提取出相关的一级依赖。

2.3 由package-lock.json构建依赖树

熟悉package-lock.json文件结构的应该清楚,该文件会列出项目所有的依赖项,包括间接依赖,直至该相关的依赖项不再有依赖项为止,因此该文件对构建项目的整体依赖树非常便利。首先定义依赖树的节点,如下所示,节点中各数据的含义解释见注释。

function DenpendencyNode(name, version, vulIndex=-1, isDev=false) {
// 节点的唯一标示,方便增加节点时快速寻找父节点
  this.identify = getMd5(Math.random().toString());
// 当前节点的深度 
  this.deep = 0;
// 依赖包的名称
  this.name = name;
// 依赖包的版本号
  this.version = version;
// 与2.1的漏洞库对应,方便判断该节点是否存在漏洞
  this.vulIndex = vulIndex;
// 是否为测试依赖项目
  this.isDev = isDev;
// 父节点
  this.parent = null;
// 子节点列表
  this.children = [];
}

然后在具体定义树的结构及相关添加元素、遍历等方法,如下所示,相关说明见注解。

// 一般通过一个空的根节点初始化整颗树
function DenpendencyTree(name, version) {
  let denpendencyNode = new DenpendencyNode(name, version);
  this._root = denpendencyNode;
}

// 深度优先遍历,可以通过定义callback函数来执行特定操作,如提取漏洞依赖链
DenpendencyTree.prototype.traverseDF = function (callback) {
  (function recurse(currentNode) {
    for (let i = 0, { length } = currentNode.children; i < length; i++) {
      recurse(currentNode.children[i]);
    }
    callback(currentNode);
  })(this._root);
};

// 检测是否包含某个节点
DenpendencyTree.prototype.contains = function (callback, traversal) {
  traversal.call(this, callback);
};

// 添加元素,通过节点的唯一标识identify来找到父节点
DenpendencyTree.prototype.add = function (name, version, vulIndex, isDev, toIdentify, traversal) {
  let child = new DenpendencyNode(name, version, vulIndex, isDev);
  let parent = null;
  let callback = function (node) {
    if (node.identify === toIdentify) {
      parent = node;
    }
  };
  this.contains(callback, traversal);
  if (parent) {
    parent.children.push(child);
    child.parent = parent;
    child.deep = parent.deep + 1;
  } else {
    throw new Error('Cannot add node to a non-existent parent.');
  }
  return [child.identify, child.deep];
};

在构建树的过程中有一点必须注意,就是对树的深度进行限制,当某个节点超过限定的深度时,则停止添加子节点,如果不进行限制则可能造成死循环,根据实际测试的时间,建议深度设置为3。

2.4 生成依赖链

生成依赖链可以通过traverseDF方法中定义的callback函数实现,对树进行遍历,当某个节点的vulIndex大于 -1 时,表明该节点存在漏洞,则遍历获取该节点的父节点,直至父节点为跟节点为止,从而构建出整条依赖链。

deConstructDenpendencyTree(results) {
// 所有的依赖链
    const dependency_lists = [];
    const denpendencyTree = results['denpendencyTree'];
    denpendencyTree.traverseDF(node => {
      if (node.vulIndex > -1) {
        let _list = [];
        while(node.parent) {
          _list.push(node);
          node = node.parent;
        }
        dependency_lists.push(_list);
      }
    });
    results['dependencyLists'] = dependency_lists;
  }

2.5 漏洞版本判断

漏洞的判断直接将依赖的版本与漏洞版本进行判断,这个判断个人觉得大多是是正确的,但是仍然有小部分判断存在问题,所以如果大家有更好的判断方法,欢迎告知。

const vulnerable_version = '>=0.2.1 <1.0.0 || >=1.2.3';
console.log(innerJudge('0.0.8', vulnerable_version));

function innerJudge(pkVersion, vulnerable_version) {
  const v_lists = vulnerable_version.split('||').map((key) => {
    const ii_list = key.trim().split(' ').map((key) => {
      return key.trim();
    });
    return ii_list;
  });
  let final_is_vul = false;
  const single_symbol = ['>', '>=', '<', '<=', '~'];
  for (const v_list of v_lists) {
    let judege_list = [];
    let last_index = 0;
    for (let i=0; i<v_list.length; i++) {
      const _item = v_list[i];
      if ((_item != '*') && (single_symbol.indexOf(_item.substring(0, 2)) == -1) && 
                            (single_symbol.indexOf(_item.substring(0, 1)) == -1) &&
                            (_item.trim() != '')) {
        let _sst = '';
        for (let j=last_index; j<=i; j++) {
          _sst = `${_sst.trim()}${v_list[j].trim()}`;
        }
        judege_list.push(_sst);
        last_index = i + 1;
      } else if (single_symbol.indexOf(_item) == -1 && _item.trim() != '') {
        judege_list.push(_item);
      } else if (_item == '*' && _item.trim() != '') {
        judege_list.push(_item);
      }
    }
    if (judege_list.length == 0) {
      judege_list = v_list;
    }
    // console.log(v_list);
    // console.log(judege_list);
    // console.log('------------');
    let is_vul = false;
    switch (judege_list.length) {
      case 1:
        is_vul = singleJudge(pkVersion, judege_list[0]);
        break;
      case 2:
        is_vul = singleJudge(pkVersion, judege_list[0]) && singleJudge(pkVersion, judege_list[1]);
        break;
      default:
        console.log('Impossible array length.', judege_list);
        break;
    }
    if (is_vul) {
      final_is_vul = is_vul;
      break;
    }
  }
  return final_is_vul;
}

function singleJudge(pkVersion, _v) {
  let is_vul = false;
  if (_v == '*') {
    is_vul = true;
  } else if (_v.indexOf('~') == 0) {
    if (pkVersion.indexOf(_v.substring(1)) == 0) {
      is_vul = true;
    }
  } else if (_v.indexOf('<=') == 0) {
    if (pkVersion <= _v.substring(2)) {
      is_vul = true;
    }
  } else if (_v.indexOf('<') == 0) {
    if (pkVersion < _v.substring(1)) {
      is_vul = true;
    }
  } else if (_v.indexOf('>=') == 0) {
    if (pkVersion >= _v.substring(2)) {
      is_vul = true;
    }
  } else if (_v.indexOf('>') == 0) {
    if (pkVersion > _v.substring(1)) {
      is_vul = true;
    }
  }
  return is_vul;
}

3.测试示例

Start pull security advisories.
Data pull progress: 6.997%
Data pull progress: 13.99%
Data pull progress: 20.99%
Data pull progress: 27.99%
Data pull progress: 34.98%
Data pull progress: 41.98%
Data pull progress: 48.98%
Data pull progress: 55.98%
Data pull progress: 62.98%
Data pull progress: 69.97%
Data pull progress: 76.97%
Data pull progress: 83.97%
Data pull progress: 90.97%
Data pull progress: 97.97%
Data pull progress: 100%
End. Security advisories size 1429. Consume time: 18.734s.
Start get base dependencies by package.json. File path: /path/to/package.json
End. Consume time: 18.879s.
Start construct dependency tree by package-lock.json. Lock file path: /path/to/package-lock.json
Max dependency deep is 3
End. Consume time: 0.9029999999999987s.
Start generate dependency lists.
End. Generate 223 dependency list. Consume time 0s

        ---------------------------------------------------------
        Result Size      : 223
        ---------------------------------------------------------

        ---------------------------------------------------------
        Severity         : low
        Package          : minimist
        Version          : 0.0.8
        VulnerableVersion: <0.2.1 || >=1.0.0 <1.2.3
        PatchedVsersion  : >=0.2.1 <1.0.0 || >=1.2.3
        DependencyPath   : mkdirp > minimist
        Dev              : false
        MoreInfo         : https://www.npmjs.com/advisories/1179
        ---------------------------------------------------------
.......

4.问题及后续优化

目前该工具基本实现了npm audit的功能,当遍历深度不深时,时间是还是可以接受的,但是算法的总体效率还是偏低,后续将对整个依赖树构建算法进行针对性优化。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK