CSP策略中unsafe eval配置引发的安全风险与解决方案

恩硕~ 安全 阅读 2,536
赞 28 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

说实话,unsafe-eval这个东西我在实际项目中用得并不多,但每次遇到相关问题都得重新折腾一遍。之前做数据可视化平台的时候,有个动态组件加载的需求,用户可以上传自定义的JavaScript代码片段来生成图表。当时被CSP(Content Security Policy)给整得够呛,报了一堆unsafe-eval相关的错误。

CSP策略中unsafe eval配置引发的安全风险与解决方案

我的解决思路其实很简单,就是把eval这种危险函数能不用就不用。如果真的需要动态执行代码,我会选择更安全的替代方案。比如,对于那种需要动态计算的场景,我一般这样处理:

// 不好的做法
function unsafeCalculate(expression) {
    return eval(expression);
}

// 更安全的做法
function safeCalculate(expression) {
    try {
        // 使用Function构造器,并限制作用域
        const calcFunction = new Function('params', 
            with(params) {
                return ${expression};
            }
        );
        return calcFunction({
            Math: Math,
            parseInt: parseInt,
            parseFloat: parseFloat
        });
    } catch (error) {
        console.error('Calculation error:', error);
        return null;
    }
}

上面这段代码的好处是限制了可访问的对象,不会让恶意代码访问到window或者其他全局对象。当然,这还不是最安全的,但如果业务确实需要动态计算,这种方式比直接eval要靠谱得多。

实际项目中的坑

上个月在做一个在线代码编辑器的时候,客户要求支持实时预览功能。这就涉及到动态执行用户输入的JavaScript代码。最开始我考虑使用eval,但CSP策略不允许unsafe-eval,浏览器直接报错了。

我折腾了半天发现,可以通过Worker线程来规避这个问题。创建一个隔离的执行环境:

class SafeExecutor {
    constructor() {
        this.workerCode = 
            self.onmessage = function(e) {
                try {
                    const result = eval(e.data.code);
                    self.postMessage({ success: true, result: result });
                } catch (error) {
                    self.postMessage({ 
                        success: false, 
                        error: error.message 
                    });
                }
            };
        ;
    }

    execute(code) {
        return new Promise((resolve, reject) => {
            const blob = new Blob([this.workerCode], { type: 'application/javascript' });
            const worker = new Worker(URL.createObjectURL(blob));
            
            worker.onmessage = (e) => {
                if (e.data.success) {
                    resolve(e.data.result);
                } else {
                    reject(new Error(e.data.error));
                }
                worker.terminate();
            };
            
            worker.postMessage({ code: code });
        });
    }
}

不过这里要注意一个问题,有些浏览器对Worker中使用eval也有限制。所以我后来改用了另外一种方案,就是通过创建iframe的方式来隔离执行环境。

还有一种情况是模板引擎相关的。之前用Handlebars的时候,某些复杂逻辑需要动态渲染,结果触发了unsafe-eval警告。后来我统一采用了预编译的方式,在构建阶段就把模板编译成纯JavaScript函数,避免了运行时的动态代码执行。

这几种错误写法,别再踩坑了

我见过太多错误的用法了,最典型的就是直接用eval处理JSON字符串:

// 错误示范 - 千万别这么写
const jsonString = '{"name": "test", "value": 123}';
const obj = eval('(' + jsonString + ')'); // 超级危险!

// 正确做法
const obj = JSON.parse(jsonString);

还有人喜欢用setTimeout传入字符串参数,这也可能触发unsafe-eval:

// 危险写法
const funcName = userInput;
setTimeout("console.log('Hello ' + " + funcName + ")", 1000);

// 更安全的做法
const safeFunc = () => {
    console.log('Hello ' + funcName);
};
setTimeout(safeFunc, 1000);

另一个常见的坑是在字符串拼接中使用变量,特别是来自用户的输入:

// 很危险!
const userCode = document.getElementById('codeInput').value;
const finalCode = return ${userCode};
const result = new Function(finalCode)();

// 建议的做法是先进行严格的输入验证和过滤
function validateAndExecute(userCode) {
    // 白名单验证,只允许特定字符
    if (!/^[a-zA-Z0-9s+-*/().]+$/.test(userCode)) {
        throw new Error('Invalid characters in code');
    }
    
    // 构造安全的执行环境
    const restrictedScope = {
        Math: Math,
        Number: Number,
        String: String
    };
    
    const func = new Function(...Object.keys(restrictedScope), return ${userCode});
    return func(...Object.values(restrictedScope));
}

这些写法看起来没什么问题,但一旦用户输入恶意代码,后果不堪设想。我之前就因为这个问题差点被老板骂死。

CSP配置的那些事儿

说到CSP配置,我也踩了不少坑。最开始我直接在meta标签里加上unsafe-eval,觉得反正解决了问题就行:

<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';">

但实际上这样做的安全风险很大。后来我改用更精确的策略,只对必要的页面开启unsafe-eval:

// 服务器端动态设置CSP头
app.use('/admin', (req, res, next) => {
    res.setHeader('Content-Security-Policy', "script-src 'self' 'unsafe-eval';");
    next();
});

// 其他路由使用严格策略
app.use((req, res, next) => {
    res.setHeader('Content-Security-Policy', "script-src 'self';");
    next();
});

这样做的好处是只有管理员界面才允许eval,普通用户的页面仍然是安全的。不过这种方式也有缺点,就是管理起来比较麻烦,需要仔细控制哪些页面真正需要unsafe-eval。

性能考虑和调试技巧

还有一点需要注意,即使你不得不使用unsafe-eval,也要考虑到性能问题。eval和new Function都会阻止JavaScript引擎的优化,导致执行效率下降。我一般会在生产环境中记录相关的性能指标:

function measureExecutionTime(fn, ...args) {
    const startTime = performance.now();
    const result = fn.apply(null, args);
    const endTime = performance.now();
    console.log(Execution time: ${endTime - startTime}ms);
    return result;
}

调试的时候我发现,Chrome DevTools的Performance面板可以清楚地看到eval调用的影响。如果你看到大片的黄色标记,基本就能确定是eval在搞鬼了。

最后一个小技巧,就是可以用try-catch包装所有的动态执行代码,并且记录详细的错误信息:

function safeDynamicExecute(code) {
    try {
        // 添加时间限制,防止无限循环
        const timeoutId = setTimeout(() => {
            throw new Error('Execution timeout');
        }, 5000);
        
        const result = eval(code);
        clearTimeout(timeoutId);
        return result;
    } catch (error) {
        console.error('Dynamic execution failed:', error);
        // 记录错误日志,便于后续分析
        logError(error, code);
        return null;
    }
}

以上是我踩坑后的总结,希望对你有帮助。这个话题的拓展用法还有很多,后续会继续分享这类博客。如果有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论