mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
🐛 fix: 修复受限环境下 secure-storage 触发 sessionStorage SecurityError
- 新增 safeGetStorage 探测能力探针,捕获 sessionStorage/localStorage 读取与写入探测时抛出的 SecurityError,返回可用的 Storage 或 null - 将顶层敏感数据迁移逻辑外层包裹 try-catch,避免 iframe/sandbox/隐私设置等受限上下文导致模块初始化阶段未捕获异常中断 - 在 migrateFromLocalStorage 中基于 safeGetStorage 动态获取 localStorage,localStorage 不可用时直接跳过迁移而非抛错 - 补充受限环境降级单元测试:模拟 sessionStorage/localStorage 属性读取被拒、以及 sessionStorage 写入失败时确保降级到 fallbackStorage 且不抛异常
This commit is contained in:
@@ -6,11 +6,32 @@ import Logger from './logger.js';
|
||||
* 防御 XSS 攻击窃取敏感数据
|
||||
*/
|
||||
|
||||
/**
|
||||
* 安全探测 Web Storage API 可用性
|
||||
* 在跨域 iframe、sandbox 上下文或用户禁用存储时,
|
||||
* 直接读取 sessionStorage/localStorage 属性可能抛出 SecurityError,
|
||||
* typeof 检查无法安全拦截此类异常。
|
||||
* @param {'sessionStorage'|'localStorage'} kind - 存储类型
|
||||
* @returns {Storage|null} 可用的存储对象,或 null
|
||||
*/
|
||||
function safeGetStorage(kind) {
|
||||
try {
|
||||
const storage = window[kind];
|
||||
// 写入测试:某些浏览器在隐身模式下返回对象但实际拒绝读写
|
||||
const testKey = `__storage_test_${kind}__`;
|
||||
storage.setItem(testKey, '1');
|
||||
storage.removeItem(testKey);
|
||||
return storage;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SecureStorage {
|
||||
constructor() {
|
||||
// 优先使用 sessionStorage (关闭浏览器即清除)
|
||||
// 若需跨标签页共享,可使用 localStorage,但必须加过期时间
|
||||
this.storage = typeof sessionStorage !== 'undefined' ? sessionStorage : null;
|
||||
this.storage = safeGetStorage('sessionStorage');
|
||||
this.fallbackStorage = new Map(); // 内存降级
|
||||
}
|
||||
|
||||
@@ -115,15 +136,16 @@ class SecureStorage {
|
||||
* @param {string} key - 键名
|
||||
*/
|
||||
migrateFromLocalStorage(key) {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
|
||||
try {
|
||||
const oldValue = localStorage.getItem(key);
|
||||
const local = safeGetStorage('localStorage');
|
||||
if (!local) return;
|
||||
|
||||
const oldValue = local.getItem(key);
|
||||
if (oldValue) {
|
||||
// 迁移数据(不带过期时间,视为短期数据)
|
||||
this.setItem(key, oldValue, 1800000); // 30分钟
|
||||
// 清除旧数据
|
||||
localStorage.removeItem(key);
|
||||
local.removeItem(key);
|
||||
Logger.log(`[SecureStorage] Migrated ${key} from localStorage`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -144,18 +166,25 @@ class SecureStorage {
|
||||
const secureStorage = new SecureStorage();
|
||||
|
||||
// 自动迁移已知的敏感数据键
|
||||
if (typeof window !== 'undefined') {
|
||||
const SENSITIVE_KEYS = [
|
||||
'giffgaff_cookie',
|
||||
'gg_esim_activationCode',
|
||||
'gg_esim_lpa',
|
||||
'gg_esim_iccid',
|
||||
'gg_esim_eid',
|
||||
'gg_access_token'
|
||||
];
|
||||
// 顶层执行必须包裹 try-catch,防止受限上下文(iframe/sandbox/隐私设置)
|
||||
// 导致 sessionStorage/localStorage 访问异常未被捕获
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const SENSITIVE_KEYS = [
|
||||
'giffgaff_cookie',
|
||||
'gg_esim_activationCode',
|
||||
'gg_esim_lpa',
|
||||
'gg_esim_iccid',
|
||||
'gg_esim_eid',
|
||||
'gg_access_token'
|
||||
];
|
||||
|
||||
// 页面加载时自动迁移
|
||||
secureStorage.migrateAll(SENSITIVE_KEYS);
|
||||
// 页面加载时自动迁移
|
||||
secureStorage.migrateAll(SENSITIVE_KEYS);
|
||||
}
|
||||
} catch (e) {
|
||||
// 降级到内存存储,模块仍可正常使用
|
||||
console.warn('[SecureStorage] Module-level initialization failed:', e.message);
|
||||
}
|
||||
|
||||
export default secureStorage;
|
||||
|
||||
@@ -125,4 +125,84 @@ describe('SecureStorage', () => {
|
||||
expect(store.getItem('bad')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('受限环境降级', () => {
|
||||
let originalSessionStorage;
|
||||
let originalLocalStorage;
|
||||
|
||||
beforeEach(() => {
|
||||
originalSessionStorage = Object.getOwnPropertyDescriptor(window, 'sessionStorage');
|
||||
originalLocalStorage = Object.getOwnPropertyDescriptor(window, 'localStorage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalSessionStorage) {
|
||||
Object.defineProperty(window, 'sessionStorage', originalSessionStorage);
|
||||
} else {
|
||||
delete window.sessionStorage;
|
||||
}
|
||||
if (originalLocalStorage) {
|
||||
Object.defineProperty(window, 'localStorage', originalLocalStorage);
|
||||
} else {
|
||||
delete window.localStorage;
|
||||
}
|
||||
});
|
||||
|
||||
it('应该在 sessionStorage 访问被拒时降级到内存存储', () => {
|
||||
// 模拟 SecurityError:属性读取抛异常
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
get() {
|
||||
throw new DOMException('Access is denied', 'SecurityError');
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// 直接构造新实例,构造函数内部调用 safeGetStorage 会捕获异常
|
||||
const SecureStorageClass = store.constructor;
|
||||
const restricted = new SecureStorageClass();
|
||||
|
||||
expect(restricted.storage).toBeNull();
|
||||
restricted.setItem('key', 'value');
|
||||
expect(restricted.getItem('key')).toBe('value');
|
||||
expect(restricted.fallbackStorage.has('key')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该在 localStorage 访问被拒时跳过迁移', () => {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
get() {
|
||||
throw new DOMException('Access is denied', 'SecurityError');
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// migrateFromLocalStorage 内部调用 safeGetStorage 会捕获异常
|
||||
expect(() => store.migrateFromLocalStorage('any_key')).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该在 sessionStorage 写入失败时降级到内存', () => {
|
||||
// 模拟 sessionStorage.setItem 拒绝写入
|
||||
store.storage = {
|
||||
getItem: jest.fn(() => null),
|
||||
setItem: jest.fn(() => {
|
||||
throw new DOMException('Quota exceeded', 'QuotaExceededError');
|
||||
}),
|
||||
removeItem: jest.fn()
|
||||
};
|
||||
|
||||
// 迁移数据:localStorage 读取成功,但 sessionStorage 写入失败
|
||||
const local = {
|
||||
getItem: jest.fn(() => 'old_value'),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn()
|
||||
};
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
get: () => local,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
store.migrateFromLocalStorage('test_key');
|
||||
// 数据应降级到 fallbackStorage
|
||||
expect(store.fallbackStorage.has('test_key')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user