Vue3 的初步处理方案
测试网站: bilibli
首先可以找到存在__vue_app__
属性
查看源码可以知道在 mount 函数中的
mount(rootContainer, isHydrate, namespace) {
if (!isMounted) {
//省略
rootContainer.__vue_app__ = app;
return getComponentPublicInstance(vnode.component);
}
}
还有一个_vnode 属性来自于mountElement
函数的
def(el, "__vnode", vnode, true);
但是我们想在想进行全局劫持,势必要混入自己的代码,Vue3 调用是createApp(rootComponent).mount(dom)
而rootContainer.__vue_app__ = app;
是在挂接之后才能出现的
我们要劫持势必要在初始化时劫持,于是可以顺着 createApp 找找思路
function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent);
}
//省略
return app;
}
这里可以看到有判断rootComponent
是否是函数,而在脚手架开发的时候,在开发完成编译的时候会通过 sfc 将 vue 的模板文件编译成组件对象,一般为 object 对象
所以大概率会走到下面的 extend 函数,那我们看看 extend 是怎么实现的const extend = Object.assign;
可以发现这部分有利用的机会!
const assign = Object.assign;
let isRun = false;
Object.assign = function (...args) {
if (args.length == 2 && args[1]?.render !== undefined && !isRun) {
let b = args[1];
const originRender = b.render;
b.render = function (...args) {
console.log("被执行了", args);
return originRender.call(this, ...args);
};
isRun = true;
}
return assign.call(this, ...args);
};
这里我利用了劫持assign
可以得到根组件的render函数
, 控制render
是因为渲染模板相对其他的属性来说可能对数据的暴露拥有更多的访问机会
但是我们该怎么混入实例呢?根据逆向最后找到了renderComponentRoot
函数的vnode.shapeFlag & 4
分支,render
模板上级调用如下
render.call(
thisProxy,
proxyToUse,
renderCache,
true ? shallowReadonly(props) : props,
setupState,
data,
ctx
);
可以看到最后一个就是 ctx!
那我们就可以通过 render 最后一个参数找到appContext
,根据appContext
再对全局混入数据!
理论建立完毕了!
// ==UserScript==
// @name Vue3 Mixin Inject
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description try to take over the world!
// @author You
// @match https://www.bilibili.com/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
const assign = Object.assign;
let isRun = false;
Object.assign = function (...args) {
if (args.length == 2 && args[1]?.render !== undefined && !isRun) {
let b = args[1];
const originRender = b.render;
let isInject = false;
b.render = function (...args) {
if (!isInject) {
args[5]["_"].appContext.mixins.push({
mounted() {
console.log("被创建了,实例数据", this.$props);
},
});
isInject = true;
}
console.log("被执行了", args);
return originRender.call(this, ...args);
};
isRun = true;
}
return assign.call(this, ...args);
};
测试一下
利用 use 进行注入
除了从createApp
的Object.assign
劫持外
通过观察还可以开发者常用的use
函数也是一个可以利用的注入点
其源代码为
use(plugin, ...options) {
if (installedPlugins.has(plugin)) {
warn$1(`Plugin has already been applied to target app.`);
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin);
plugin.install(app, ...options);
} else if (isFunction(plugin)) {
installedPlugins.add(plugin);
plugin(app, ...options);
} else {
warn$1(
`A plugin must either be a function or an object with an "install" function.`
);
}
return app;
}
可以看到使用了installedPlugins
而该变量是一个WeakSet
const installedPlugins = /* @__PURE__ */ new WeakSet();
可知针对WeakSet
进行劫持得到Plugin
,对Plugin
进行一层包装依然可以实现夺取appContext
对象
const originWeakSet = WeakSet;
unsafeWindow.WeakSet = function () {
const instance = new originWeakSet();
const has = instance.has;
instance.has = function (...args) {
// 劫持位置
return has.call(this, ...args);
};
return instance;
};
但是由于时间原因作者就不进行补足了,感兴趣大家可以对这方面进行补充并且更新过来~