前端自检神器Self检测的实战应用与踩坑总结

西门浚博 安全 阅读 2,550
赞 6 收藏
二维码
手机扫码查看
反馈

Self检测,我一般选这种方式

之前做安全防护的时候遇到Self检测的需求,查了不少资料发现方案还挺多的。说实话,刚开始我也搞不清哪种方式更好,折腾了好几个小时才把各个方案摸透。现在回过头看,其实几种方式差别还挺明显的,有些坑我当时就踩过。

前端自检神器Self检测的实战应用与踩坑总结

Window.self vs iframe检测,各有千秋

最常用的两种Self检测方案,一种是直接用window.self判断,另一种是iframe检测。我比较喜欢用window.self的方式,因为代码简单,但是iframe检测有时候更准确。

先看看window.self的实现:

function checkSelf() {
    if (window.self !== window.top) {
        console.log('页面被嵌套了');
        return false;
    }
    
    // 更严格的检测
    try {
        window.self.document;
        return true;
    } catch(e) {
        console.log('跨域检测失败', e);
        return false;
    }
}

// 使用
if (!checkSelf()) {
    alert('页面不允许被嵌套');
}

这种方案的好处是简单直接,一个判断就搞定。但是有个坑就是如果遇到特别复杂的嵌套关系,可能检测不够准确。比如有些恶意站点会用多层iframe包装,这时候window.self的判断就不够用了。

再说说iframe检测方案:

function detectFrameBusting() {
    let inIframe = false;
    
    try {
        inIframe = window.self !== window.top;
        
        // 检测是否有尝试脱离iframe的行为
        if (inIframe && window.location.hostname === document.referrer.split('/')[2]) {
            // 检测是否尝试修改parent.location
            const originalParentLocation = window.parent.location.href;
            
            setTimeout(() => {
                if (window.parent.location.href !== originalParentLocation) {
                    console.log('检测到frame busting行为');
                }
            }, 100);
        }
    } catch(e) {
        // 跨域情况下无法访问parent
        inIframe = true;
    }
    
    return inIframe;
}

iframe检测的好处是能检测到更多细节,比如有没有frame busting行为。但是代码复杂度高了不少,而且跨域情况下能获取的信息有限。

Document.referrer配合检测,更全面

单独用window.self可能不够准确,配合document.referrer一起检测效果更好。我一般会把这两种方式结合起来。

function comprehensiveSelfCheck() {
    // 基础self检测
    const isNested = window.self !== window.top;
    
    // referrer检测
    const hasReferrer = document.referrer && document.referrer !== '';
    const referrerDomain = hasReferrer ? 
        document.referrer.split('/')[2] : null;
    
    // 当前域名
    const currentDomain = window.location.hostname;
    
    // 检测逻辑
    if (isNested && hasReferrer && referrerDomain !== currentDomain) {
        return {
            isSafe: false,
            reason: '外部嵌套',
            details: { isNested, referrerDomain, currentDomain }
        };
    }
    
    // 检测是否在可信域名内
    const trustedDomains = ['trusted-domain.com', 'another-trusted.com'];
    if (isNested && hasReferrer && !trustedDomains.includes(referrerDomain)) {
        return {
            isSafe: false,
            reason: '非可信域名嵌套',
            details: { referrerDomain }
        };
    }
    
    return {
        isSafe: true,
        reason: '正常访问',
        details: { isNested, hasReferrer, referrerDomain }
    };
}

// 使用
const checkResult = comprehensiveSelfCheck();
console.log('Self检测结果:', checkResult);

if (!checkResult.isSafe) {
    alert(访问异常: ${checkResult.reason});
}

这种组合检测的方式虽然代码多了点,但是准确率确实提高不少。特别是配合可信域名列表,能过滤掉大部分恶意嵌套。

CSP头部检测,额外防护

除了JavaScript检测,还可以配合CSP头部来增强防护。这个我在实际项目中用得不多,主要是考虑到兼容性问题。

// 动态检查CSP策略
function checkCSP() {
    const metaTags = document.getElementsByTagName('meta');
    for (let i = 0; i < metaTags.length; i++) {
        if (metaTags[i].getAttribute('http-equiv') === 'Content-Security-Policy') {
            const cspValue = metaTags[i].getAttribute('content');
            if (cspValue && cspValue.includes('frame-ancestors')) {
                // 解析frame-ancestors规则
                const frameAncestors = cspValue.match(/frame-ancestorss+([^;]+)/i);
                if (frameAncestors) {
                    return {
                        hasFrameAncestors: true,
                        allowedOrigins: frameAncestors[1].trim().split(/s+/)
                    };
                }
            }
        }
    }
    
    return {
        hasFrameAncestors: false,
        allowedOrigins: []
    };
}

// 服务端设置CSP头部
// Content-Security-Policy: frame-ancestors 'self';

说实话,CSP检测的实际效果要看浏览器支持情况,老版本浏览器基本没戏。而且动态检查CSP策略也比较复杂,一般还是建议服务端直接设置。

我的选型逻辑:简单实用优先

经过多次实践,我现在主要用window.self + document.referrer的组合。简单来说就是先用window.self判断是否被嵌套,如果是的话再用referrer判断来源是否可信。这套方案覆盖了大部分场景,而且代码量适中。

具体选择要考虑几个因素:

  • 项目复杂度:简单项目用window.self就够了
  • 安全要求:高安全需求建议组合检测
  • 兼容性:老旧浏览器可能需要降级处理
  • 维护成本:代码越复杂后期维护难度越大

对于大部分业务场景,我建议这样实现:

class SelfDetector {
    constructor(options = {}) {
        this.trustedOrigins = options.trustedOrigins || [];
        this.checkInterval = options.checkInterval || 1000;
        this.isChecking = false;
    }
    
    basicCheck() {
        try {
            // 基础self检测
            const isTop = window.self === window.top;
            
            if (!isTop) {
                // 检测嵌套深度
                let depth = 0;
                let current = window;
                while (current !== current.parent) {
                    depth++;
                    current = current.parent;
                    if (depth > 10) break; // 防止无限循环
                }
                
                return {
                    isSafe: false,
                    type: 'nested',
                    depth: depth
                };
            }
            
            return { isSafe: true, type: 'normal' };
        } catch(e) {
            return {
                isSafe: false,
                type: 'cross_domain_access_error'
            };
        }
    }
    
    fullCheck() {
        const basicResult = this.basicCheck();
        
        if (!basicResult.isSafe && basicResult.type === 'nested') {
            // 检查referrer是否可信
            if (document.referrer) {
                try {
                    const referrerOrigin = new URL(document.referrer).origin;
                    if (this.trustedOrigins.includes(referrerOrigin)) {
                        basicResult.isSafe = true;
                        basicResult.type = 'trusted_nested';
                    }
                } catch(e) {
                    // referrer格式不正确,按异常处理
                }
            }
        }
        
        return basicResult;
    }
    
    autoCheck() {
        if (this.isChecking) return;
        
        this.isChecking = true;
        setInterval(() => {
            const result = this.fullCheck();
            if (!result.isSafe) {
                this.handleUnsafe(result);
            }
        }, this.checkInterval);
    }
    
    handleUnsafe(result) {
        console.warn('Self检测异常:', result);
        
        // 可以选择重定向或者提示用户
        if (result.type === 'nested' || result.type === 'cross_domain_access_error') {
            // 这里可以记录日志或者报警
            fetch('https://jztheme.com/api/security-alert', {
                method: 'POST',
                body: JSON.stringify({
                    type: 'self_detection_violation',
                    details: result
                })
            });
        }
    }
}

// 使用示例
const detector = new SelfDetector({
    trustedOrigins: ['https://trusted-domain.com'],
    checkInterval: 2000
});

const result = detector.fullCheck();
console.log('Self检测结果:', result);

if (!result.isSafe) {
    // 根据不同情况处理
    if (result.type === 'nested') {
        document.body.innerHTML = '<div>页面不允许被嵌套访问</div>';
    }
}

这样一套下来,基本能满足大部分项目的Self检测需求。重点是要根据实际业务场景调整策略,不能盲目追求完美方案。

以上是我的对比总结,有不同看法欢迎评论区交流

Self检测这块确实没有标准答案,每种方案都有适用场景。关键是要清楚各种方案的局限性,选择最适合当前项目的方案。我踩过的坑基本都在文中提到了,希望能帮你少走点弯路。

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

暂无评论