|
|
|
@ -1,141 +1,179 @@
|
|
|
|
<template>
|
|
|
|
<template>
|
|
|
|
<div class="p-5 space-y-5 !bg-gray-100">
|
|
|
|
<div class="p-5 space-y-5 !bg-gray-100">
|
|
|
|
<el-card header="如何修改闭包内对象属性">
|
|
|
|
<el-card header="Hack:修改/扩展闭包内的对象">
|
|
|
|
<div class="mb-4">
|
|
|
|
<div class="mb-4">
|
|
|
|
<p class="text-gray-600 mb-2">
|
|
|
|
<p class="text-gray-600 mb-2">
|
|
|
|
在 JavaScript 中,闭包内的变量通常是私有的。如果闭包内定义了一个对象,我们有几种方式可以修改它的属性:
|
|
|
|
场景描述:有一个闭包函数(假设是第三方库提供的),内部包含一个私有对象,且<b>仅暴露了一个根据 key 获取值的方法</b>。
|
|
|
|
</p>
|
|
|
|
<br />
|
|
|
|
<ul class="list-disc ml-6 text-sm text-gray-500">
|
|
|
|
我们在<b>不修改该函数源码</b>的情况下,试图:
|
|
|
|
<li>通过闭包返回的 Setter 方法进行修改。</li>
|
|
|
|
</p>
|
|
|
|
<li>如果闭包直接返回了该对象的引用,可以直接修改其属性。</li>
|
|
|
|
<ul class="list-disc ml-6 text-sm text-gray-500">
|
|
|
|
<li>通过原型链或特定的 Hack 方式(不推荐但存在)。</li>
|
|
|
|
<li>1. <b>新增属性</b>:让闭包返回我们自定义的新属性(利用原型链污染)。</li>
|
|
|
|
</ul>
|
|
|
|
<li>2. <b>修改属性</b>:修改闭包内部对象的属性(利用引用类型泄露)。</li>
|
|
|
|
</div>
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">目标闭包函数(模拟不可变代码)</el-divider>
|
|
|
|
|
|
|
|
<pre class="bg-gray-800 text-white p-4 rounded text-xs overflow-x-auto">
|
|
|
|
|
|
|
|
// 这是一个即使你看不见源码,也无法修改的闭包
|
|
|
|
|
|
|
|
const getValue = (function() {
|
|
|
|
|
|
|
|
const privateData = {
|
|
|
|
|
|
|
|
id: 1001,
|
|
|
|
|
|
|
|
owner: 'admin',
|
|
|
|
|
|
|
|
// 注意:这是一个对象类型的属性
|
|
|
|
|
|
|
|
config: {
|
|
|
|
|
|
|
|
theme: 'dark',
|
|
|
|
|
|
|
|
editable: false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return function(key) {
|
|
|
|
|
|
|
|
// 漏洞:没有过滤 key,直接返回属性
|
|
|
|
|
|
|
|
return privateData[key];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">示例展示</el-divider>
|
|
|
|
<el-divider content-position="left">Hack 演示</el-divider>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
<div class="bg-white p-4 rounded border">
|
|
|
|
<!-- 操作区 -->
|
|
|
|
<h4 class="font-bold mb-2">闭包内部状态:</h4>
|
|
|
|
<div class="space-y-4">
|
|
|
|
<pre class="bg-gray-50 p-2 rounded text-xs">{{ JSON.stringify(closureState, null, 2) }}</pre>
|
|
|
|
<el-card shadow="never" class="!border-blue-200">
|
|
|
|
</div>
|
|
|
|
<template #header>
|
|
|
|
|
|
|
|
<span class="font-bold text-blue-600">挑战1:新增属性 (原型链污染)</span>
|
|
|
|
<div class="flex gap-4">
|
|
|
|
</template>
|
|
|
|
<el-button
|
|
|
|
<p class="text-xs text-gray-500 mb-2">
|
|
|
|
type="primary"
|
|
|
|
原理:利用 JS 对象属性查找机制。如果对象本身没有该属性,会去原型链(Object.prototype)上找。
|
|
|
|
@click="handleUpdateViaSetter"
|
|
|
|
</p>
|
|
|
|
>
|
|
|
|
<div class="flex gap-2 mb-2">
|
|
|
|
通过 Setter 修改年龄 (age + 1)
|
|
|
|
<el-input v-model="hackKey" placeholder="新属性名 (如: hackID)" size="small" />
|
|
|
|
</el-button>
|
|
|
|
<el-input v-model="hackValue" placeholder="新属性值" size="small" />
|
|
|
|
<el-button
|
|
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
|
|
@click="handleUpdateDirectly"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
直接修改引用属性 (name = 'New Name')
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
|
|
@click="resetState"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
重置状态
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<el-button type="danger" size="small" @click="applyPrototypePollution">
|
|
|
|
|
|
|
|
污染 Object.prototype
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
<el-button size="small" @click="testGetValue(hackKey)">
|
|
|
|
|
|
|
|
尝试 getValue('{{ hackKey }}')
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">核心代码原理</el-divider>
|
|
|
|
<el-card shadow="never" class="!border-green-200">
|
|
|
|
<pre class="bg-gray-800 text-white p-4 rounded text-xs overflow-x-auto">
|
|
|
|
<template #header>
|
|
|
|
function createClosure() {
|
|
|
|
<span class="font-bold text-green-600">挑战2:修改属性 (引用泄露)</span>
|
|
|
|
// 闭包内部的对象
|
|
|
|
</template>
|
|
|
|
const privateObj = {
|
|
|
|
<p class="text-xs text-gray-500 mb-2">
|
|
|
|
name: 'Initial Name',
|
|
|
|
原理:虽然无法修改 'owner' (基本类型),但 'config' 是对象。getValue('config') 返回的是引用,修改它会影响闭包内部!
|
|
|
|
age: 25
|
|
|
|
</p>
|
|
|
|
};
|
|
|
|
<el-button type="success" size="small" @click="modifyRefProperty">
|
|
|
|
|
|
|
|
修改 config.theme = 'light'
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
<el-button size="small" @click="checkConfig">
|
|
|
|
|
|
|
|
查看当前 config
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
<!-- 结果展示区 -->
|
|
|
|
// 方式1:通过方法修改
|
|
|
|
<div class="bg-white p-4 rounded border h-full">
|
|
|
|
updateAge(newAge) {
|
|
|
|
<h4 class="font-bold mb-2">测试结果 / 控制台日志:</h4>
|
|
|
|
privateObj.age = newAge;
|
|
|
|
<div class="bg-gray-900 text-green-400 p-2 rounded text-xs h-64 overflow-y-auto font-mono">
|
|
|
|
},
|
|
|
|
<div v-for="(log, index) in logs" :key="index" class="mb-1 border-b border-gray-800 pb-1">
|
|
|
|
// 方式2:直接暴露对象引用
|
|
|
|
<span class="text-gray-500">[{{ log.time }}]</span> {{ log.msg }}
|
|
|
|
getData() {
|
|
|
|
</div>
|
|
|
|
return privateObj;
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
};
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
</el-card>
|
|
|
|
</pre
|
|
|
|
</div>
|
|
|
|
>
|
|
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
<script setup lang="ts">
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
import { ref, onUnmounted } from 'vue';
|
|
|
|
import { message } from '@/utils/message';
|
|
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
/**
|
|
|
|
// 模拟的闭包环境 (假设这是第三方库代码,不可修改)
|
|
|
|
* 模拟一个产生闭包的函数
|
|
|
|
// ==========================================
|
|
|
|
*/
|
|
|
|
const getValue = (function() {
|
|
|
|
function createClosure() {
|
|
|
|
const privateData = {
|
|
|
|
const privateObj = {
|
|
|
|
id: 1001,
|
|
|
|
name: '张三',
|
|
|
|
owner: 'admin',
|
|
|
|
age: 20,
|
|
|
|
config: {
|
|
|
|
};
|
|
|
|
theme: 'dark',
|
|
|
|
|
|
|
|
editable: false
|
|
|
|
return {
|
|
|
|
}
|
|
|
|
// 暴露一个获取数据的方法(返回引用)
|
|
|
|
} as any; // TS ignore for demo
|
|
|
|
getData() {
|
|
|
|
|
|
|
|
return privateObj;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
// 暴露一个专门的修改方法
|
|
|
|
|
|
|
|
setAge(age: number) {
|
|
|
|
|
|
|
|
privateObj.age = age;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
// 还原
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
|
|
|
privateObj.name = '张三';
|
|
|
|
|
|
|
|
privateObj.age = 20;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const closureManager = createClosure();
|
|
|
|
return function(key: string) {
|
|
|
|
const closureState = ref({ name: '', age: 0 });
|
|
|
|
console.log(`[System] 正在访问 key: ${key}`);
|
|
|
|
|
|
|
|
return privateData[key];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地响应式状态以供显示
|
|
|
|
const logs = ref<{time: string, msg: string}[]>([]);
|
|
|
|
const refreshDisplay = () => {
|
|
|
|
const hackKey = ref('hackerSignature');
|
|
|
|
const data = closureManager.getData();
|
|
|
|
const hackValue = ref('I was here');
|
|
|
|
// 注意:为了让 Vue 追踪变化,我们需要重新赋值
|
|
|
|
|
|
|
|
closureState.value = { ...data };
|
|
|
|
function addLog(msg: string) {
|
|
|
|
|
|
|
|
const time = new Date().toLocaleTimeString();
|
|
|
|
|
|
|
|
logs.value.unshift({ time, msg });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 挑战1:原型链污染
|
|
|
|
|
|
|
|
const applyPrototypePollution = () => {
|
|
|
|
|
|
|
|
if (!hackKey.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 核心 Hack 代码
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
|
|
Object.prototype[hackKey.value] = hackValue.value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addLog(`已执行: Object.prototype.${hackKey.value} = '${hackValue.value}'`);
|
|
|
|
|
|
|
|
addLog('警告:这不仅影响了闭包对象,也污染了全局所有对象!');
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpdateViaSetter = () => {
|
|
|
|
const testGetValue = (key: string) => {
|
|
|
|
const currentAge = closureManager.getData().age;
|
|
|
|
if (!key) return;
|
|
|
|
closureManager.setAge(currentAge + 1);
|
|
|
|
const result = getValue(key);
|
|
|
|
refreshDisplay();
|
|
|
|
addLog(`调用 getValue('${key}') 返回: ${JSON.stringify(result)}`);
|
|
|
|
message('通过 Setter 修改成功', { type: 'success' });
|
|
|
|
|
|
|
|
|
|
|
|
if (result === hackValue.value) {
|
|
|
|
|
|
|
|
addLog('-> 成功!虽然 privateData 中没有这个 key,但通过原型链取到了值。');
|
|
|
|
|
|
|
|
} else if (result === undefined) {
|
|
|
|
|
|
|
|
addLog('-> 返回 undefined。可能未注入或被遮蔽。');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
addLog('-> 返回了现有值(如果是 owner 等自有属性,无法被原型链覆盖)。');
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpdateDirectly = () => {
|
|
|
|
// 挑战2:引用修改
|
|
|
|
const data = closureManager.getData();
|
|
|
|
const modifyRefProperty = () => {
|
|
|
|
// 因为返回的是对象引用,直接修改属性会同步到闭包内部
|
|
|
|
// 1. 获取引用
|
|
|
|
data.name = '被直接修改的名称 ' + Math.floor(Math.random() * 100);
|
|
|
|
const configObj = getValue('config');
|
|
|
|
refreshDisplay();
|
|
|
|
addLog(`获取到 config 引用: ${JSON.stringify(configObj)}`);
|
|
|
|
message('通过引用直接修改成功', { type: 'success' });
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 修改引用指向的对象的属性
|
|
|
|
|
|
|
|
if (configObj && typeof configObj === 'object') {
|
|
|
|
|
|
|
|
configObj.theme = 'light';
|
|
|
|
|
|
|
|
configObj.hackedBy = 'User';
|
|
|
|
|
|
|
|
addLog(`已执行: configObj.theme = 'light'`);
|
|
|
|
|
|
|
|
addLog('由于是引用传递,闭包内部的 privateData.config 已经变了。');
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetState = () => {
|
|
|
|
const checkConfig = () => {
|
|
|
|
closureManager.reset();
|
|
|
|
const config = getValue('config');
|
|
|
|
refreshDisplay();
|
|
|
|
addLog(`当前闭包内的 config: ${JSON.stringify(config)}`);
|
|
|
|
message('状态已重置');
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
// 清理副作用:页面销毁时还原原型链,以免影响其他页面
|
|
|
|
refreshDisplay();
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
|
|
if (hackKey.value) {
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
|
|
delete Object.prototype[hackKey.value];
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
<style scoped>
|
|
|
|
pre {
|
|
|
|
|
|
|
|
font-family: 'Courier New', Courier, monospace;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
</style>
|
|
|
|
|