mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
fix: restrict access to file system via API (#3956)
This commit is contained in:
parent
91fe4853a5
commit
bcb41e514f
@ -11,11 +11,11 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
|
||||
}
|
||||
|
||||
readSnapshotFile(filepath: string): Promise<string | null> {
|
||||
return rpc().readFile(filepath)
|
||||
return rpc().readSnapshotFile(filepath)
|
||||
}
|
||||
|
||||
saveSnapshotFile(filepath: string, snapshot: string): Promise<void> {
|
||||
return rpc().writeFile(filepath, snapshot, true)
|
||||
return rpc().saveSnapshotFile(filepath, snapshot)
|
||||
}
|
||||
|
||||
resolvePath(filepath: string): Promise<string> {
|
||||
@ -27,10 +27,6 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
|
||||
}
|
||||
|
||||
removeSnapshotFile(filepath: string): Promise<void> {
|
||||
return rpc().removeFile(filepath)
|
||||
}
|
||||
|
||||
async prepareDirectory(dirPath: string): Promise<void> {
|
||||
await rpc().createDirectory(dirPath)
|
||||
return rpc().removeSnapshotFile(filepath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './ty
|
||||
|
||||
export class SnapshotManager {
|
||||
summary: SnapshotSummary = undefined!
|
||||
resolvedPaths = new Set<string>()
|
||||
extension = '.snap'
|
||||
|
||||
constructor(public options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>) {
|
||||
@ -26,7 +27,9 @@ export class SnapshotManager {
|
||||
)
|
||||
})
|
||||
|
||||
return resolver(testPath, this.extension)
|
||||
const path = resolver(testPath, this.extension)
|
||||
this.resolvedPaths.add(path)
|
||||
return path
|
||||
}
|
||||
|
||||
resolveRawPath(testPath: string, rawPath: string) {
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { dirname, join } from 'pathe'
|
||||
import naturalCompare from 'natural-compare'
|
||||
import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
|
||||
import {
|
||||
@ -128,13 +127,6 @@ function printBacktickString(str: string): string {
|
||||
return `\`${escapeBacktickString(str)}\``
|
||||
}
|
||||
|
||||
export async function ensureDirectoryExists(environment: SnapshotEnvironment, filePath: string) {
|
||||
try {
|
||||
await environment.prepareDirectory(join(dirname(filePath)))
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
export function normalizeNewlines(string: string) {
|
||||
return string.replace(/\r\n|\r/g, '\n')
|
||||
}
|
||||
@ -157,7 +149,6 @@ export async function saveSnapshotFile(
|
||||
if (skipWriting)
|
||||
return
|
||||
|
||||
await ensureDirectoryExists(environment, snapshotPath)
|
||||
await environment.saveSnapshotFile(
|
||||
snapshotPath,
|
||||
content,
|
||||
@ -175,7 +166,6 @@ export async function saveSnapshotFileRaw(
|
||||
if (skipWriting)
|
||||
return
|
||||
|
||||
await ensureDirectoryExists(environment, snapshotPath)
|
||||
await environment.saveSnapshotFile(
|
||||
snapshotPath,
|
||||
content,
|
||||
|
||||
@ -3,7 +3,6 @@ export interface SnapshotEnvironment {
|
||||
getHeader(): string
|
||||
resolvePath(filepath: string): Promise<string>
|
||||
resolveRawPath(testPath: string, rawPath: string): Promise<string>
|
||||
prepareDirectory(dirPath: string): Promise<void>
|
||||
saveSnapshotFile(filepath: string, snapshot: string): Promise<void>
|
||||
readSnapshotFile(filepath: string): Promise<string | null>
|
||||
removeSnapshotFile(filepath: string): Promise<void>
|
||||
|
||||
@ -24,7 +24,7 @@ watch(() => props.file,
|
||||
draft.value = false
|
||||
return
|
||||
}
|
||||
code.value = await client.rpc.readFile(props.file.filepath) || ''
|
||||
code.value = await client.rpc.readTestFile(props.file.filepath) || ''
|
||||
serverCode.value = code.value
|
||||
draft.value = false
|
||||
},
|
||||
@ -116,7 +116,7 @@ watch([cm, failed], ([cmValue]) => {
|
||||
|
||||
async function onSave(content: string) {
|
||||
hasBeenEdited.value = true
|
||||
await client.rpc.writeFile(props.file!.filepath, content)
|
||||
await client.rpc.saveTestFile(props.file!.filepath, content)
|
||||
serverCode.value = content
|
||||
draft.value = false
|
||||
}
|
||||
|
||||
@ -46,21 +46,26 @@ export function createStaticClient(): VitestClient {
|
||||
return {
|
||||
code: id,
|
||||
source: '',
|
||||
map: null,
|
||||
}
|
||||
},
|
||||
readFile: async (id) => {
|
||||
return Promise.resolve(id)
|
||||
},
|
||||
onDone: noop,
|
||||
onCollected: asyncNoop,
|
||||
onTaskUpdate: noop,
|
||||
writeFile: asyncNoop,
|
||||
rerun: asyncNoop,
|
||||
updateSnapshot: asyncNoop,
|
||||
removeFile: asyncNoop,
|
||||
createDirectory: asyncNoop,
|
||||
resolveSnapshotPath: asyncNoop,
|
||||
snapshotSaved: asyncNoop,
|
||||
onAfterSuiteRun: asyncNoop,
|
||||
onCancel: asyncNoop,
|
||||
getCountOfFailedTests: () => 0,
|
||||
sendLog: asyncNoop,
|
||||
resolveSnapshotRawPath: asyncNoop,
|
||||
readSnapshotFile: asyncNoop,
|
||||
saveSnapshotFile: asyncNoop,
|
||||
readTestFile: asyncNoop,
|
||||
removeSnapshotFile: asyncNoop,
|
||||
} as WebSocketHandlers
|
||||
|
||||
ctx.rpc = rpc as any as BirpcReturn<WebSocketHandlers>
|
||||
|
||||
@ -127,6 +127,9 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null {
|
||||
// normalize Windows path (\ -> /)
|
||||
file = resolve(file)
|
||||
|
||||
if (method)
|
||||
method = method.replace(/__vite_ssr_import_\d+__\./g, '')
|
||||
|
||||
return {
|
||||
method,
|
||||
file,
|
||||
|
||||
@ -69,25 +69,36 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
|
||||
resolveSnapshotRawPath(testPath, rawPath) {
|
||||
return ctx.snapshot.resolveRawPath(testPath, rawPath)
|
||||
},
|
||||
removeFile(id) {
|
||||
return fs.unlink(id)
|
||||
async readSnapshotFile(snapshotPath) {
|
||||
if (!ctx.snapshot.resolvedPaths.has(snapshotPath) || !existsSync(snapshotPath))
|
||||
return null
|
||||
return fs.readFile(snapshotPath, 'utf-8')
|
||||
},
|
||||
createDirectory(id) {
|
||||
return fs.mkdir(id, { recursive: true })
|
||||
},
|
||||
async readFile(id) {
|
||||
if (!existsSync(id))
|
||||
async readTestFile(id) {
|
||||
if (!ctx.state.filesMap.has(id) || !existsSync(id))
|
||||
return null
|
||||
return fs.readFile(id, 'utf-8')
|
||||
},
|
||||
async saveTestFile(id, content) {
|
||||
// can save only already existing test file
|
||||
if (!ctx.state.filesMap.has(id) || !existsSync(id))
|
||||
return
|
||||
return fs.writeFile(id, content, 'utf-8')
|
||||
},
|
||||
async saveSnapshotFile(id, content) {
|
||||
if (!ctx.snapshot.resolvedPaths.has(id))
|
||||
return
|
||||
await fs.mkdir(dirname(id), { recursive: true })
|
||||
return fs.writeFile(id, content, 'utf-8')
|
||||
},
|
||||
async removeSnapshotFile(id) {
|
||||
if (!ctx.snapshot.resolvedPaths.has(id) || !existsSync(id))
|
||||
return
|
||||
return fs.unlink(id)
|
||||
},
|
||||
snapshotSaved(snapshot) {
|
||||
ctx.snapshot.add(snapshot)
|
||||
},
|
||||
async writeFile(id, content, ensureDir) {
|
||||
if (ensureDir)
|
||||
await fs.mkdir(dirname(id), { recursive: true })
|
||||
return await fs.writeFile(id, content, 'utf-8')
|
||||
},
|
||||
async rerun(files) {
|
||||
await ctx.rerunFiles(files)
|
||||
},
|
||||
|
||||
@ -21,10 +21,11 @@ export interface WebSocketHandlers {
|
||||
resolveSnapshotRawPath(testPath: string, rawPath: string): string
|
||||
getModuleGraph(id: string): Promise<ModuleGraphData>
|
||||
getTransformResult(id: string): Promise<TransformResultWithSource | undefined>
|
||||
readFile(id: string): Promise<string | null>
|
||||
writeFile(id: string, content: string, ensureDir?: boolean): Promise<void>
|
||||
removeFile(id: string): Promise<void>
|
||||
createDirectory(id: string): Promise<string | undefined>
|
||||
readSnapshotFile(id: string): Promise<string | null>
|
||||
readTestFile(id: string): Promise<string | null>
|
||||
saveTestFile(id: string, content: string): Promise<void>
|
||||
saveSnapshotFile(id: string, content: string): Promise<void>
|
||||
removeSnapshotFile(id: string): Promise<void>
|
||||
snapshotSaved(snapshot: SnapshotResult): void
|
||||
rerun(files: string[]): Promise<void>
|
||||
updateSnapshot(file?: File): Promise<void>
|
||||
|
||||
@ -8,6 +8,7 @@ import { resolveApiServerConfig } from '../../node/config'
|
||||
import { CoverageTransform } from '../../node/plugins/coverageTransform'
|
||||
import type { WorkspaceProject } from '../../node/workspace'
|
||||
import { MocksPlugin } from '../../node/plugins/mocks'
|
||||
import { resolveFsAllow } from '../../node/plugins/utils'
|
||||
|
||||
export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) {
|
||||
const root = project.config.root
|
||||
@ -44,7 +45,13 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us
|
||||
|
||||
config.server = server
|
||||
config.server.fs ??= {}
|
||||
config.server.fs.strict = false
|
||||
config.server.fs.allow = config.server.fs.allow || []
|
||||
config.server.fs.allow.push(
|
||||
...resolveFsAllow(
|
||||
project.ctx.config.root,
|
||||
project.ctx.server.config.configFile,
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
resolve: {
|
||||
|
||||
@ -17,6 +17,8 @@ export async function createVitest(mode: VitestRunMode, options: UserConfig, vit
|
||||
? resolve(root, options.config)
|
||||
: await findUp(configFiles, { cwd: root } as any)
|
||||
|
||||
options.config = configPath
|
||||
|
||||
const config: ViteInlineConfig = {
|
||||
logLevel: 'error',
|
||||
configFile: configPath,
|
||||
|
||||
@ -12,7 +12,7 @@ import { GlobalSetupPlugin } from './globalSetup'
|
||||
import { CSSEnablerPlugin } from './cssEnabler'
|
||||
import { CoverageTransform } from './coverageTransform'
|
||||
import { MocksPlugin } from './mocks'
|
||||
import { deleteDefineConfig, hijackVitePluginInject, resolveOptimizerConfig } from './utils'
|
||||
import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow, resolveOptimizerConfig } from './utils'
|
||||
import { VitestResolver } from './vitestResolver'
|
||||
|
||||
export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('test')): Promise<VitePlugin[]> {
|
||||
@ -87,6 +87,9 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
|
||||
open,
|
||||
hmr: false,
|
||||
preTransformRequests: false,
|
||||
fs: {
|
||||
allow: resolveFsAllow(getRoot(), testConfig.config),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { builtinModules } from 'node:module'
|
||||
import { version as viteVersion } from 'vite'
|
||||
import { searchForWorkspaceRoot, version as viteVersion } from 'vite'
|
||||
import type { DepOptimizationOptions, ResolvedConfig, UserConfig as ViteConfig } from 'vite'
|
||||
import { dirname } from 'pathe'
|
||||
import type { DepsOptimizationOptions, InlineConfig } from '../../types'
|
||||
|
||||
export function resolveOptimizerConfig(_testOptions: DepsOptimizationOptions | undefined, viteOptions: DepOptimizationOptions | undefined, testConfig: InlineConfig) {
|
||||
@ -84,3 +85,9 @@ export function hijackVitePluginInject(viteConfig: ResolvedConfig) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveFsAllow(projectRoot: string, rootConfigFile: string | false | undefined) {
|
||||
if (!rootConfigFile)
|
||||
return [searchForWorkspaceRoot(projectRoot)]
|
||||
return [dirname(rootConfigFile), searchForWorkspaceRoot(projectRoot)]
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { CSSEnablerPlugin } from './cssEnabler'
|
||||
import { SsrReplacerPlugin } from './ssrReplacer'
|
||||
import { GlobalSetupPlugin } from './globalSetup'
|
||||
import { MocksPlugin } from './mocks'
|
||||
import { deleteDefineConfig, hijackVitePluginInject, resolveOptimizerConfig } from './utils'
|
||||
import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow, resolveOptimizerConfig } from './utils'
|
||||
import { VitestResolver } from './vitestResolver'
|
||||
|
||||
interface WorkspaceOptions extends UserWorkspaceConfig {
|
||||
@ -69,6 +69,12 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp
|
||||
open: false,
|
||||
hmr: false,
|
||||
preTransformRequests: false,
|
||||
fs: {
|
||||
allow: resolveFsAllow(
|
||||
project.ctx.config.root,
|
||||
project.ctx.server.config.configFile,
|
||||
),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
env,
|
||||
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -1840,6 +1840,15 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/vitest
|
||||
|
||||
test/restricted:
|
||||
devDependencies:
|
||||
jsdom:
|
||||
specifier: ^22.1.0
|
||||
version: 22.1.0
|
||||
vitest:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/vitest
|
||||
|
||||
test/run:
|
||||
devDependencies:
|
||||
vite:
|
||||
@ -6771,7 +6780,7 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/node': 20.5.0
|
||||
'@types/node': 18.16.19
|
||||
playwright-core: 1.28.0
|
||||
dev: true
|
||||
|
||||
@ -8928,7 +8937,7 @@ packages:
|
||||
resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.1
|
||||
'@types/node': 18.16.19
|
||||
'@types/node': 20.5.0
|
||||
dev: true
|
||||
|
||||
/@types/fs-extra@9.0.13:
|
||||
@ -9318,7 +9327,7 @@ packages:
|
||||
/@types/ws@8.5.5:
|
||||
resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==}
|
||||
dependencies:
|
||||
'@types/node': 18.16.19
|
||||
'@types/node': 20.5.0
|
||||
dev: true
|
||||
|
||||
/@types/yargs-parser@21.0.0:
|
||||
|
||||
12
test/restricted/package.json
Normal file
12
test/restricted/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@vitest/test-restricted",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^22.1.0",
|
||||
"vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
3
test/restricted/src/math.js
Normal file
3
test/restricted/src/math.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function multiply(a, b) {
|
||||
return a * b
|
||||
}
|
||||
7
test/restricted/tests/basic.spec.js
Normal file
7
test/restricted/tests/basic.spec.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { multiply } from '../src/math'
|
||||
|
||||
it('2 x 2 = 4', () => {
|
||||
expect(multiply(2, 2)).toBe(4)
|
||||
expect(multiply(2, 2)).toBe(Math.sqrt(16))
|
||||
})
|
||||
29
test/restricted/vitest.config.ts
Normal file
29
test/restricted/vitest.config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { resolve } from 'pathe'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
// simulates restrictive FS
|
||||
name: 'restrict-fs',
|
||||
config() {
|
||||
return {
|
||||
server: {
|
||||
fs: {
|
||||
allow: [
|
||||
resolve(__dirname, 'src'),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['tests/**/*.spec.{js,ts}'],
|
||||
setupFiles: [
|
||||
'./vitest.setup.js',
|
||||
],
|
||||
},
|
||||
})
|
||||
1
test/restricted/vitest.setup.js
Normal file
1
test/restricted/vitest.setup.js
Normal file
@ -0,0 +1 @@
|
||||
globalThis.SOME_TEST_VARIABLE = '3'
|
||||
Loading…
x
Reference in New Issue
Block a user