2021年12月

egg.js 中helper方法下的shtml 会删除无域名的src链接处理解决方案

开发网页或多或少会碰到要保存富文本内容的情况.但凡碰到需要保存富文本,服务端就必须开启xss防护.否则是非常不安全的.

egg.js默认集成了XSS框架,附上框架链接:
https://jsxss.com/zh/index.html
使用方法是 const safehtml = this.ctx.helper.shtml(html)
返回的html就是过滤后的html内容,可以安全保存在数据库中了.
里面有各种对不同的tag,不同属性的配置,这里就不举例了,大部分情况下默认配置就行,个别情况要改的话,根据默认配置增减就行.这里附上源码中的默认配置,以便查阅

https://github.com/leizongmin/js-xss/blob/master/lib/default.js

这里要说的主要内容是在egg.js中调用shtml,如果src或者href中的内容是'/'开头的没有域名,他是直接剔除掉的,附上源码:

    if (isWhiteAttr && (name === 'href' || name === 'src')) {
        if (!value) {
          return;
        }

        value = String(value);

        if (value[0] === '/' || value[0] === '#') {
          return;
        }

        const hostname = utils.getFromUrl(value, 'hostname');
        if (!hostname) {
          return;
        }

        // If we don't have our hostname in the app.security.domainWhiteList,
        // Just check for `shtmlConfig.domainWhiteList` and `ctx.whiteList`.
        if (!isSafeDomain(hostname, domainWhiteList)) {
          // Check for `shtmlConfig.domainWhiteList` first (duplicated now)
          if (shtmlConfig.domainWhiteList && shtmlConfig.domainWhiteList.length !== 0) {
            app.deprecate('[egg-security] `config.helper.shtml.domainWhiteList` has been deprecate. Please use `config.security.domainWhiteList` instead.');
            shtmlConfig.domainWhiteList = shtmlConfig.domainWhiteList.map(domain => domain.toLowerCase());
            if (!isSafeDomain(hostname, shtmlConfig.domainWhiteList)) {
              return '';
            }
          } else {
            return '';
          }
        }
      }

源码可以看到 if(value[0] === '/' || value[0] === '#'){ return }

但是一些情况下,在开发环境中,我们就是不想加域名,而是通过代理去实现图片访问,这个时候图片下的src属性值就是'/'开头.

通过源码可以看到,他合并了egg.js中的config,然后在最后判断了config中是否有onTagAttr方法,用来重写他的onTagAttr方法

    ...
    const shtmlConfig = utils.merge(this.app.config.helper.shtml, securityOptions.shtml);
    ...
    // avoid overriding user configuration 'onTagAttr'
    if (shtmlConfig.onTagAttr) {
      const original = shtmlConfig.onTagAttr;
      shtmlConfig.onTagAttr = function() {
        const result = original.apply(this, arguments);
        if (result !== undefined) {
          return result;
        }
        return shtmlConfig[BUILD_IN_ON_TAG_ATTR].apply(this, arguments);

      };
    } else {
      shtmlConfig.onTagAttr = shtmlConfig[BUILD_IN_ON_TAG_ATTR];
    }

我这里在config.default.js中添加了helper.shtml.onTagAttr方法,然后在config.prod.js文件中把他覆盖掉了,这样开发环境就可以使用'/'开头了.附上config.default.js中的配置代码:

const config  = {
// ...
    helper: {
      shtml: {
        onTagAttr(tag, name, value, isWhiteAttr) {
          if (isWhiteAttr && (name === 'href' || name === 'src')) {
            if (!value) {
              return
            }

            value = String(value)

            // 测试环境通过/做代理,不要过滤掉.
            if (value[0] === '/') {
              return `${name}="${value}"`
            }
          }
        },
      },
    },
// ...
}