CodeVetter · Repo Unpack · Risk map
vuejs/core
The security surface is narrow but sharp: three code paths build render functions with `new Function`, two paths assign `innerHTML`, and the SFC compiler reads arbitrary files off disk and `require`s user-supplied preprocessors. Structurally, the biggest blast radius is `renderer.ts` (2670 lines, the single implementation behind every platform) and the `__COMPAT__` layer, which threads conditionals through core runtime code that most contributors never exercise. CI hygiene is unusually strong (pinned action SHAs, `persist-credentials: false`, org-membership gating on the ecosystem-ci trigger), so the residual risks are code-level rather than pipeline-level.
- Runtime template compilation evaluates generated code with `new Function('Vue', code)(runtimeDom)` (or `new Function(code)()` in global builds) in both the full build and the compat build — the intended design, but it means any path that can influence a template string achieves arbitrary JS execution.
- Two in-DOM template paths are annotated `__UNSAFE__` by the authors and explicitly push trust onto the user: `compileToFunction` reading `el.innerHTML` for a `#selector` template, and `app.mount` copying `container.innerHTML` into `component.template`.
- Trusted Types are only best-effort: `nodeOps.ts` creates a `'vue'` policy when `window.trustedTypes` exists and otherwise `unsafeToTrustedHTML` degrades to the identity function, so v-html/innerHTML writes pass through unwrapped in browsers or configurations without TT.
- `stringifyStatic` evaluates constant expressions at compile time with `new Function(\`return (${exp.content})\`)()`, and `validateExpression` similarly constructs a `new Function` to syntax-check user expressions — both run inside the build process, so a malicious SFC executes code in the developer's toolchain.
- `compiler-sfc` performs unconstrained file IO outside the repo root: `resolveFS` reads any path the type resolver walks to (imports, tsconfig `references`, `globalTypeFiles`), bounded only by whatever `options.fs` the host plugin supplies.
- Style preprocessors are loaded by dynamic `require` with a caller-injectable `preprocessCustomRequire`, so the module actually loaded for `lang="scss"` depends on the host's resolution context rather than a pinned dependency.
- `packages/runtime-core/src/renderer.ts` is 2670 lines and holds mountComponent, setupRenderEffect, the full patch switch and `patchKeyedChildren` in one closure-scoped factory; every change there affects DOM, SSR hydration, runtime-test and any third-party custom renderer simultaneously.
- The `__COMPAT__` layer is woven through hot core paths — mountComponent's `compatMountInstance`, finishComponentSetup's `inline-template` prop lookup, setupRenderEffect's `hook:beforeMount`/`hook:mounted` emissions — yet `packages/vue-compat/**` is excluded from coverage collection, so these branches are the least-verified code in the runtime.
- Suspense + async-setup interaction is a recurring correctness hotspot: the head commit had to change an unmount guard from `hydratedEl && !instance.scope.active` to `!instance.scope.active`, and `effect.ts` carries a long comment about effects orphaned when a component is unmounted while awaiting inside `withAsyncContext`.
- The scheduler's recursion guard is dev-only: `checkRecursiveUpdates` is wired in via `const check = __DEV__ ? ... : NOOP`, so an infinite update loop that only manifests in production silently spins instead of warning.
- Module-level mutable singletons create ordering hazards across tests and micro-frontends: `renderer`/`enabledHydration` in runtime-dom, `compile`/`installWithProxy` in component.ts, `compileCache` in vue/src/index.ts, and `globalVersion`/`activeSub` in reactivity are all process-global.
- The `ecosystem-ci-trigger` workflow accepts `/ecosystem-ci run` from issue comments with `issues: write`/`pull-requests: write` permissions, mitigated by a `checkMembershipForUser({ org: 'vuejs' })` gate that throws for non-members — the gate is the only thing between a comment and a triggered pipeline.
- Workflow supply-chain hygiene is deliberately tight: every third-party action is pinned to a full commit SHA, all checkouts use `persist-credentials: false`, and `release.yml` is guarded by both `github.repository == 'vuejs/core'` and a protected `Release` environment.
- Because the SFC playground's vite config `spawnSync`s git and copies built bundles out of `../../packages` at bundle time, a playground build silently fails late (throwing 'not built. Run "nr build vue -f esm-browser" first') rather than at config load — a fragile coupling between two build systems.