AlexZ33 / lessions

自己练习的各种demo和课程
12 stars 2 forks source link

JavaScript安全指南 #129

Open AlexZ33 opened 1 year ago

AlexZ33 commented 1 year ago
目录 - [1 JavaScript页面类](#1) * [I. 代码实现](#1.1) + [1.1 原生DOM API的安全操作](#1.1.1) + [1.2 流行框架/库的安全操作](#1.1.2) + [1.3 页面重定向](#1.1.3) + [1.4 JSON解析/动态执行](#1.1.4) + [1.5 跨域通讯](#1.1.5) * [II. 配置&环境](#1.2) + [2.1 敏感/配置信息](#1.2.1) + [2.2 第三方组件/资源](#1.2.2) + [2.3 纵深安全防护](#1.2.3) - [2 Node.js后台类](#2) * [I. 代码实现](#2.1) + [1.1 输入验证](#2.1.1) + [1.2 执行命令](#2.1.2) + [1.3 文件操作](#2.1.3) + [1.4 网络请求](#2.1.4) + [1.5 数据输出](#2.1.5) + [1.6 响应输出](#2.1.6) + [1.7 执行代码](#2.1.7) + [1.8 Web跨域](#2.1.8) + [1.9 SQL操作](#2.1.9) + [1.10 NoSQL操作](#2.1.10) + [1.11 服务器端渲染(SSR)](#2.1.11) + [1.12 URL跳转](#2.1.12) + [1.13 Cookie与登录态](#2.1.13) * [II. 配置&环境](#2.2) + [2.1 敏感/配置信息](#2.2.1) + [2.2 第三方组件/资源](#2.2.2) + [2.3 纵深安全防护](#2.2.3)

JavaScript页面类

I. 代码实现

1.1 原生DOM API的安全操作

1.1.1【必须】HTML标签操作,限定/过滤传入变量值

// 假设 params 为用户输入, text 为 DOM 节点
// bad:将不可信内容带入HTML标签操作
const { user } = params;
// ...
text.innerHTML = `Follow @${user}`;

// good: innerHTML操作前,对特殊字符编码转义
function htmlEncode(iStr) {
    let sStr = iStr;
    sStr = sStr.replace(/&/g, "&");
    sStr = sStr.replace(/>/g, ">");
    sStr = sStr.replace(/</g, "&lt;");
    sStr = sStr.replace(/"/g, "&quot;");
    sStr = sStr.replace(/'/g, "&#39;");
    return sStr;
}

let { user } = params;
user = htmlEncode(user);
// ...
text.innerHTML = `Follow @${user}`;

// good: 使用安全的DOM API替代innerHTML
const { user } = params;
// ...
text.innerText = `Follow @${user}`;

1.1.2【必须】HTML属性操作,限定/过滤传入变量值

// good: setAttribute操作前,限定引入资源的目标地址
function addExternalCss(e) {
    const t = document.createElement('link');
    t.setAttribute('href', e),
    t.setAttribute('rel', 'stylesheet'),
    t.setAttribute('type', 'text/css'),
    document.head.appendChild(t)
}

function validURL(sUrl) {
    return !!((/^(https?:\/\/)?[\w\-.]+\.(qq|tencent)\.com($|\/|\\)/i).test(sUrl) || (/^[\w][\w/.\-_%]+$/i).test(sUrl) || (/^[/\\][^/\\]/i).test(sUrl));
}

let sUrl = "https://evil.com/1.css"
if (validURL(sUrl)) {
    addExternalCss(sUrl);
}

1.2 流行框架/库的安全操作

1.2.1【必须】限定/过滤传入jQuery不安全函数的变量值

// bad:将不可信内容,带入jQuery不安全函数.after()操作
const { user } = params;
// ...
$("p").after(user);

// good: jQuery不安全函数.html()操作前,对特殊字符编码转义
function htmlEncode(iStr) {
    let sStr = iStr;
    sStr = sStr.replace(/&/g, "&amp;");
    sStr = sStr.replace(/>/g, "&gt;");
    sStr = sStr.replace(/</g, "&lt;");
    sStr = sStr.replace(/"/g, "&quot;");
    sStr = sStr.replace(/'/g, "&#39;");
    return sStr;
}

// const user = params.user;
user = htmlEncode(user);
// ...
$("p").html(user);

1.2.2【必须】限定/过滤传入Vue.js不安全函数的变量值

// bad:直接渲染外部传入的不可信内容
<div v-html="userProvidedHtml"></div>

// good:使用富文本过滤库处理不可信内容后渲染
<!-- 使用 -->
<div v-xss-html="{'mode': 'whitelist', dirty: html, options: options}" ></div>

<!-- 配置 -->
<script>
    new Vue({
    el: "#app",
    data: {
        options: {
            whiteList: {
                a: ["href", "title", "target", "class", "id"],
                div: ["class", "id"],
                span: ["class", "id"],
                img: ["src", "alt"],
            },
        },
    },
});
</script>
// bad:v-bind允许外部可控值,自定义CSS属性及数值
<a v-bind:href="sanitizedUrl" v-bind:style="userProvidedStyles">
click me
</a>

// good:v-bind只允许外部提供特性、可控的CSS属性值
<a v-bind:href="sanitizedUrl" v-bind:style="{
color: userProvidedColor,
background: userProvidedBackground
}" >
click me
</a>

1.3 页面重定向

1.3.1【必须】限定跳转目标地址

// bad: 跳转至外部可控的不可信地址
const sTargetUrl = getURLParam("target");
location.replace(sTargetUrl);

// good: 白名单限定重定向地址
function validURL(sUrl) {
    return !!((/^(https?:\/\/)?[\w\-.]+\.(qq|tencent)\.com($|\/|\\)/i).test(sUrl) || (/^[\w][\w/.\-_%]+$/i).test(sUrl) || (/^[/\\][^/\\]/i).test(sUrl));
}

const sTargetUrl = getURLParam("target");
if (validURL(sTargetUrl)) {
    location.replace(sTargetUrl);
}

// good: 制定重定向地址为固定值
const sTargetUrl = "http://www.qq.com";
location.replace(sTargetUrl);

1.4 JSON解析/动态执行

1.4.1【必须】使用安全的JSON解析方式

// bad: 直接调用eval解析json
const sUserInput = getURLParam("json_val");
const jsonstr1 = `{"name":"a","company":"b","value":"${sUserInput}"}`;
const json1 = eval(`(${jsonstr1})`);

// good: 使用JSON.parse解析
const sUserInput = getURLParam("json_val");
JSON.parse(sUserInput, (k, v) => {
    if (k === "") return v;
    return v * 2;
});

// good: 低版本浏览器,使用安全的Polyfill封装(基于eval)
<script src="https://github.com/douglascrockford/JSON-js/blob/master/json2.js"></script>;
const sUserInput = getURLParam("json_val");
JSON.parse(sUserInput);

1.5 跨域通讯

1.5.1【必须】使用安全的前端跨域通信方式

1.5.2【必须】使用postMessage应限定Origin

// bad: 使用indexOf校验Origin值
window.addEventListener("message", (e) => {
    if (~e.origin.indexOf("https://a.qq.com")) {
    // ...
    } else {
    // ...
    }
});

// good: 使用postMessage时,限定Origin,且使用===判断
window.addEventListener("message", (e) => {
    if (e.origin === "https://a.qq.com") {
    // ...
    }
});

II. 配置&环境

2.1 敏感/配置信息

2.1.1【必须】禁止明文硬编码AK/SK

2.2 第三方组件/资源

2.2.1【必须】使用可信范围内的统计组件

2.2.2 【必须】禁止引入非可信来源的第三方JS

2.3 纵深安全防护

2.3.1【推荐】部署CSP,并启用严格模式

Node.js后台类

I. 代码实现

1.1 输入验证

1.1.1【必须】按类型进行数据校验

// bad:未进行输入验证
Router.get("/vulxss", (req, res) => {
    const { txt } = req.query;
    res.set("Content-Type", "text/html");
    res.send({
        data: txt,
    });
});

// good:按数据类型,进行输入验证
const Router = require("express").Router();
const validator = require("validator");

Router.get("/email_with_validator", (req, res) => {
    const txt = req.query.txt || "";
    if (validator.isEmail(txt)) {
        res.send({
            data: txt,
        });
    } else {
        res.send({ err: 1 });
    }
});

关联漏洞:纵深防护措施 - 安全性增强特性

1.2 执行命令

1.2.1 【必须】使用child_process执行系统命令,应限定或校验命令和参数的内容

const Router = require("express").Router();
const validator = require("validator");
const { exec } = require('child_process');

// bad:未限定或过滤,直接执行命令
Router.get("/vul_cmd_inject", (req, res) => {
    const txt = req.query.txt || "echo 1";
    exec(txt, (err, stdout, stderr) => {
        if (err) { res.send({ err: 1 }) }
        res.send({stdout, stderr});
    });
});

// good:通过白名单,限定外部可执行命令范围
Router.get("/not_vul_cmd_inject", (req, res) => {
    const txt = req.query.txt || "echo 1";
  const phone = req.query.phone || "";
    const cmdList = {
        sendmsg: "./sendmsg "
    };
    if (txt in cmdList && validator.isMobilePhone(phone)) {
        exec(cmdList[txt] + phone, (err, stdout, stderr) => {
          if (err) { res.send({ err: 1 }) };
          res.send({stdout, stderr});
        });
    } else {
        res.send({
            err: 1,
            tips: `you can use '${Object.keys(cmdList)}'`,
        });
    }
});

// good:执行命令前,过滤/转义指定符号
Router.get("/not_vul_cmd_inject", (req, res) => {
    const txt = req.query.txt || "echo 1";
  let phone = req.query.phone || "";
    const cmdList = {
        sendmsg: "./sendmsg "
    };
    phone = phone.replace(/(\||;|&|\$\(|\(|\)|>|<|\`|!)/gi,"");
    if (txt in cmdList) {
        exec(cmdList[txt] + phone, (err, stdout, stderr) => {
          if (err) { res.send({ err: 1 }) };
          res.send({stdout, stderr});
        });
    } else {
        res.send({
            err: 1,
            tips: `you can use '${Object.keys(cmdList)}'`,
        });
    }
});

关联漏洞:高风险 - 任意命令执行

1.3 文件操作

1.3.1 【必须】限定文件操作的后缀范围

1.3.2 【必须】校验并限定文件路径范围

const fs = require("fs");
const path = require("path");
let filename = req.query.ufile;
let root = '/data/ufile';

// bad:未检查文件名/路径
fs.readFile(root + filename, (err, data) => {
    if (err) {
        return console.error(err);
    }
    console.log(`异步读取: ${data.toString()}`);
});

// bad:使用path处理过后的路径参数值做校验,仍可能有路径穿越风险
filename = path.join(root, filename);
if (filename.indexOf("..") < 0) {
    fs.readFile(filename, (err, data) => {
        if (err) {
            return console.error(err);
        }
        console.log(data.toString());
    });
};

// good:检查了文件名/路径,是否包含路径穿越字符
if (filename.indexOf("..") < 0) {
    filename = path.join(root, filename);
    fs.readFile(filename, (err, data) => {
        if (err) {
            return console.error(err);
        }
        console.log(data.toString());
    });
};

1.3.3 【必须】安全地处理上传文件名

1.3.4 【必须】敏感资源文件,应有加密、鉴权和水印等加固措施

1.4 网络请求

1.4.1 【必须】限定访问网络资源地址范围

1.4.2 【推荐】请求网络资源,应加密传输

关联漏洞:高风险 - SSRF,高风险 - HTTP劫持

1.5 数据输出

1.5.1 【必须】高敏感信息禁止存储、展示

1.5.2【必须】一般敏感信息脱敏展示

1.5.3 【推荐】返回的字段按业务需要输出

关联漏洞:高风险 - 用户敏感信息泄露

1.6 响应输出

1.6.1 【必须】设置正确的HTTP响应包类型

1.6.2 【必须】添加安全响应头

1.6.3 【必须】外部输入拼接到响应页面前,进行编码处理

场景 编码规则
输出点在HTML标签之间 需要对以下6个特殊字符进行HTML实体编码(&, <, >, ", ',/)。
示例:
& --> &amp;
< --> &lt;
>--> &gt;
" --> &quot;
' --> &#x27;
/ --> &#x2F;
输出点在HTML标签普通属性内(如href、src、style等,on事件除外) 要对数据进行HTML属性编码。
编码规则:除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为&#xHH;(以&#x开头,HH则是指该字符对应的十六进制数字,分号作为结束符)
输出点在JS内的数据中 需要进行js编码
编码规则:
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \xHH (以 \x 开头,HH则是指该字符对应的十六进制数字)
Tips:这种场景仅限于外部数据拼接在js里被引号括起来的变量值中。除此之外禁止直接将代码拼接在js代码中。
输出点在CSS中(Style属性) 需要进行CSS编码
编码规则:
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \HH (以 \ 开头,HH则是指该字符对应的十六进制数字)
输出点在URL属性中 对这些数据进行URL编码
Tips:除此之外,所有链接类属性应该校验其协议。禁止JavaScript、data和Vb伪协议。

1.6.4 【必须】响应禁止展示物理资源、程序内部代码逻辑等敏感信息

// bad
Access denied for user 'xxx'@'xx.xxx.xxx.162' (using password: NO)"

1.6.5 【推荐】添加安全纵深防御措施

// good:使用helmet组件安全地配置响应头
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet());

// good:正确配置Content-Type、添加了安全响应头,引入了CSP
Router.get("/", (req, res) => {
    res.header("Content-Type", "application/json");
    res.header("X-Content-Type-Options", "nosniff");
    res.header("X-Frame-Options", "SAMEORIGIN");
    res.header("Content-Security-Policy", "script-src 'self'");
});

关联漏洞:中风险 - XSS、中风险 - 跳转漏洞

1.7 执行代码

1.7.1 【必须】安全的代码执行方式

关联漏洞:高风险 - 代码执行漏洞

1.8 Web跨域

1.8.1 【必须】限定JSONP接口的callback字符集范围

1.8.2 【必须】安全的CORS配置

// good:使用全等于,校验请求的Origin
if (req.headers.origin === 'https://domain.qq.com') {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Access-Control-Allow-Credentials', true);
}

关联漏洞:中风险 - XSS,中风险 - CSRF,中风险 - CORS配置不当

1.9 SQL操作

1.9.1 【必须】SQL语句默认使用预编译并绑定变量

1.9.2 【必须】安全的ORM操作

//bad: adonisjs ORM
//参考:https://adonisjs.com/docs/3.2/security-introduction#_sql_injection
const username = request.param("username");
const users = yield Database
  .table("users")
  .where(Database.raw(`username = ${username}`));

//good: adonisjs ORM
const username = request.param("username");
const users = yield Database
  .table('users')
  .where(Database.raw("username = ?", [username]));
/*
good
假设该api用于插入用户的基本信息,使用传入的req.body通过Sequelize的create方法实现
假设User包含字段:username,email,isAdmin,
其中,isAdmin将会用于是否系统管理员的鉴权,默认值为false
*/
// Sequelize: 只允许变更username、email字段值
User.create(req.body, { fields: ["username", "email"] }).then((user) => {
    // handle the rest..
});

为什么要这么做? 在上述案例中,若不限定fields值,攻击者将可传入{"username":"boo","email":"foo@boo.com","isAdmin":true}将自己变为Admin,产生垂直越权漏洞。

关联漏洞:高风险 - SQL注入,中风险 - Mass Assignment 逻辑漏洞

1.10 NoSQL操作

1.10.1 【必须】校验参数值类型

// bad:执行NOSQL操作前,未作任何判断
app.post("/", (req, res) => {
    db.users.find({ username: req.body.username, password: req.body.password }, (err, users) => {
    // **TODO:** handle the rest
    });
});

// good:在进入nosql前先判断`__USER_INPUT__`是否为字符串。
app.post("/", (req, res) => {
    if (req.body.username && typeof req.body.username !== "string") {
        return new Error("username must be a string");
    }
    if (req.body.password && typeof req.body.password !== "string") {
        return new Error("password must be a string");
    }
    db.users.find({ username: req.body.username, password: req.body.password }, (err, users) => {
        // **TODO:** handle the rest
    });
});

为什么要这么做?

JavaScript中,从http或socket接收的数据可能不是单纯的字符串,而是被黑客精心构造的对象(Object)。在本例中:

  • 期望接收的POST数据:username=foo&password=bar
  • 期望的等价条件查询sql语句:select * from users where username = 'foo' and password = 'bar'
  • 黑客的精心构造的攻击POST数据:username[$ne]=null&password[$ne]=null或JSON格式:{"username": {"$ne": null},"password": {"$ne": null}}
  • 黑客篡改后的等价条件查询sql语句:select * from users where username != null & password != null
  • 黑客攻击结果:绕过正常逻辑,在不知道他人的username/password的情况登录他人账号。

1.10.2 【必须】NoSQL操作前,应校验权限/角色

// 使用express、mongodb(mongoose)实现的删除文章demo
// bad:在删除文章前未做权限校验
app.post("/deleteArticle", (req, res) => {
    db.articles.deleteOne({ article_id: req.body.article_id }, (err, users) => {
        // TODO: handle the rest
    });
});

// good:进入nosql语句前先进行权限校验
app.post("/deleteArticle", (req, res) => {
    checkPriviledge(ctx.uin, req.body.article_id);
    db.articles.deleteOne({ article_id: req.body.article_id }, (err, users) => {
        // TODO: handle the rest
    });
});

关联漏洞:高风险 - 越权操作,高风险 - NoSQL注入

1.11 服务器端渲染(SSR)

1.11.1 【必须】安全的Vue服务器端渲染(Vue SSR)

// bad: 将用户输入替换进模板
const app = new Vue({
    template: appTemplate.replace("word", __USER_INPUT__),
});
renderer.renderToString(app);
// bad: 渲染后的html再拼接不受信的外部输入
return new Promise(((resolve) => {
    renderer.renderToString(component, (err, html) => {
        let htmlOutput = html;
        htmlOutput += `${__USER_INPUT__}`;
        resolve(htmlOutput);
    });
}));

1.11.2 【必须】安全地使用EJS、LoDash、UnderScore进行服务器端渲染

1.11.3 【必须】在自行实现状态存储容器并将其JSON.Stringify序列化后注入到HTML时,必须进行安全过滤

1.12 URL跳转

1.12.1【必须】限定跳转目标地址

// 使用express实现的登录成功后的回调跳转页面

// bad: 未校验页面重定向地址
app.get("/login", (req, res) => {
    // 若未登录用户访问其他页面,则让用户导向到该处理函数进行登录
  // 使用参数loginCallbackUrl记录先前尝试访问的url,在登录成功后跳转回loginCallbackUrl:
    const { loginCallbackUrl } = req.query;
    if (loginCallbackUrl) {
        res.redirect(loginCallbackUrl);
    }
});

// good: 白名单限定重定向地址
function isValidURL(sUrl) {
    return !!((/^(https?:\/\/)?[\w\-.]+\.(qq|tencent)\.com($|\/|\\)/i).test(sUrl) || (/^[\w][\w/.\-_%]+$/i).test(sUrl) || (/^[/\\][^/\\]/i).test(sUrl));
}
app.get("/login", (req, res) => {
    // 若未登录用户访问其他页面,则让用户导向到该处理函数进行登录
  // 使用参数loginCallbackUrl记录先前尝试访问的url,在登录成功后跳转回loginCallbackUrl:
    const { loginCallbackUrl } = req.query;
    if (loginCallbackUrl && isValidUrl(loginCallbackUrl)) {
        res.redirect(loginCallbackUrl);
    }
});

// good: 白名单限定重定向地址,通过返回html实现
function isValidURL(sUrl) {
    return !!((/^(https?:\/\/)?[\w\-.]+\.(qq|tencent)\.com($|\/|\\)/i).test(sUrl) || (/^[\w][\w/.\-_%]+$/i).test(sUrl) || (/^[/\\][^/\\]/i).test(sUrl));
}
app.get("/login", (req, res) => {
    // 若未登录用户访问其他页面,则让用户导向到该处理函数进行登录
  // 使用参数loginCallbackUrl记录先前尝试访问的url,在登录成功后跳转回loginCallbackUrl:
    const { loginCallbackUrl } = req.query;
    if (loginCallbackUrl && isValidUrl(loginCallbackUrl)) {
        // 使用encodeURI,过滤左右尖括号与双引号,防止逃逸出包裹的双引号
        const redirectHtml = `<script>location.href = "${encodeURI(loginCallbackUrl)}";</script>`;
        res.end(redirectHtml);
    }
});

关联漏洞:中风险 - 任意URL跳转漏洞

1.13 Cookie与登录态

1.13.1【推荐】为Cookies中存储的关键登录态信息添加http-only保护

关联漏洞:纵深防护措施 - 安全性增强特性

II. 配置&环境

2.1 依赖库

2.1.1【必须】使用安全的依赖库

2.2 运行环境

2.2.1 【必须】使用非root用户运行Node.js

2.3 配置信息

2.3.1【必须】禁止硬编码认证凭证

2.3.2【必须】禁止硬编码IP配置

2.3.3【必须】禁止硬编码员工敏感信息