Skip to content

Commit bcb41e5

Browse files
authoredAug 15, 2023
fix: restrict access to file system via API (#3956)
1 parent 91fe485 commit bcb41e5

File tree

20 files changed

+143
-49
lines changed

20 files changed

+143
-49
lines changed
 

‎packages/browser/src/client/snapshot.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
1111
}
1212

1313
readSnapshotFile(filepath: string): Promise<string | null> {
14-
return rpc().readFile(filepath)
14+
return rpc().readSnapshotFile(filepath)
1515
}
1616

1717
saveSnapshotFile(filepath: string, snapshot: string): Promise<void> {
18-
return rpc().writeFile(filepath, snapshot, true)
18+
return rpc().saveSnapshotFile(filepath, snapshot)
1919
}
2020

2121
resolvePath(filepath: string): Promise<string> {
@@ -27,10 +27,6 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
2727
}
2828

2929
removeSnapshotFile(filepath: string): Promise<void> {
30-
return rpc().removeFile(filepath)
31-
}
32-
33-
async prepareDirectory(dirPath: string): Promise<void> {
34-
await rpc().createDirectory(dirPath)
30+
return rpc().removeSnapshotFile(filepath)
3531
}
3632
}

‎packages/snapshot/src/manager.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './ty
33

44
export class SnapshotManager {
55
summary: SnapshotSummary = undefined!
6+
resolvedPaths = new Set<string>()
67
extension = '.snap'
78

89
constructor(public options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>) {
@@ -26,7 +27,9 @@ export class SnapshotManager {
2627
)
2728
})
2829

29-
return resolver(testPath, this.extension)
30+
const path = resolver(testPath, this.extension)
31+
this.resolvedPaths.add(path)
32+
return path
3033
}
3134

3235
resolveRawPath(testPath: string, rawPath: string) {

‎packages/snapshot/src/port/utils.ts

-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { dirname, join } from 'pathe'
98
import naturalCompare from 'natural-compare'
109
import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
1110
import {
@@ -128,13 +127,6 @@ function printBacktickString(str: string): string {
128127
return `\`${escapeBacktickString(str)}\``
129128
}
130129

131-
export async function ensureDirectoryExists(environment: SnapshotEnvironment, filePath: string) {
132-
try {
133-
await environment.prepareDirectory(join(dirname(filePath)))
134-
}
135-
catch { }
136-
}
137-
138130
export function normalizeNewlines(string: string) {
139131
return string.replace(/\r\n|\r/g, '\n')
140132
}
@@ -157,7 +149,6 @@ export async function saveSnapshotFile(
157149
if (skipWriting)
158150
return
159151

160-
await ensureDirectoryExists(environment, snapshotPath)
161152
await environment.saveSnapshotFile(
162153
snapshotPath,
163154
content,
@@ -175,7 +166,6 @@ export async function saveSnapshotFileRaw(
175166
if (skipWriting)
176167
return
177168

178-
await ensureDirectoryExists(environment, snapshotPath)
179169
await environment.saveSnapshotFile(
180170
snapshotPath,
181171
content,

‎packages/snapshot/src/types/environment.ts

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ export interface SnapshotEnvironment {
33
getHeader(): string
44
resolvePath(filepath: string): Promise<string>
55
resolveRawPath(testPath: string, rawPath: string): Promise<string>
6-
prepareDirectory(dirPath: string): Promise<void>
76
saveSnapshotFile(filepath: string, snapshot: string): Promise<void>
87
readSnapshotFile(filepath: string): Promise<string | null>
98
removeSnapshotFile(filepath: string): Promise<void>

‎packages/ui/client/components/views/ViewEditor.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ watch(() => props.file,
2424
draft.value = false
2525
return
2626
}
27-
code.value = await client.rpc.readFile(props.file.filepath) || ''
27+
code.value = await client.rpc.readTestFile(props.file.filepath) || ''
2828
serverCode.value = code.value
2929
draft.value = false
3030
},
@@ -116,7 +116,7 @@ watch([cm, failed], ([cmValue]) => {
116116
117117
async function onSave(content: string) {
118118
hasBeenEdited.value = true
119-
await client.rpc.writeFile(props.file!.filepath, content)
119+
await client.rpc.saveTestFile(props.file!.filepath, content)
120120
serverCode.value = content
121121
draft.value = false
122122
}

‎packages/ui/client/composables/client/static.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,26 @@ export function createStaticClient(): VitestClient {
4646
return {
4747
code: id,
4848
source: '',
49+
map: null,
4950
}
5051
},
51-
readFile: async (id) => {
52-
return Promise.resolve(id)
53-
},
5452
onDone: noop,
5553
onCollected: asyncNoop,
5654
onTaskUpdate: noop,
5755
writeFile: asyncNoop,
5856
rerun: asyncNoop,
5957
updateSnapshot: asyncNoop,
60-
removeFile: asyncNoop,
61-
createDirectory: asyncNoop,
6258
resolveSnapshotPath: asyncNoop,
6359
snapshotSaved: asyncNoop,
60+
onAfterSuiteRun: asyncNoop,
61+
onCancel: asyncNoop,
62+
getCountOfFailedTests: () => 0,
63+
sendLog: asyncNoop,
64+
resolveSnapshotRawPath: asyncNoop,
65+
readSnapshotFile: asyncNoop,
66+
saveSnapshotFile: asyncNoop,
67+
readTestFile: asyncNoop,
68+
removeSnapshotFile: asyncNoop,
6469
} as WebSocketHandlers
6570

6671
ctx.rpc = rpc as any as BirpcReturn<WebSocketHandlers>

‎packages/utils/src/source-map.ts

+3
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null {
127127
// normalize Windows path (\ -> /)
128128
file = resolve(file)
129129

130+
if (method)
131+
method = method.replace(/__vite_ssr_import_\d+__\./g, '')
132+
130133
return {
131134
method,
132135
file,

‎packages/vitest/src/api/setup.ts

+23-12
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,36 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
6969
resolveSnapshotRawPath(testPath, rawPath) {
7070
return ctx.snapshot.resolveRawPath(testPath, rawPath)
7171
},
72-
removeFile(id) {
73-
return fs.unlink(id)
74-
},
75-
createDirectory(id) {
76-
return fs.mkdir(id, { recursive: true })
72+
async readSnapshotFile(snapshotPath) {
73+
if (!ctx.snapshot.resolvedPaths.has(snapshotPath) || !existsSync(snapshotPath))
74+
return null
75+
return fs.readFile(snapshotPath, 'utf-8')
7776
},
78-
async readFile(id) {
79-
if (!existsSync(id))
77+
async readTestFile(id) {
78+
if (!ctx.state.filesMap.has(id) || !existsSync(id))
8079
return null
8180
return fs.readFile(id, 'utf-8')
8281
},
82+
async saveTestFile(id, content) {
83+
// can save only already existing test file
84+
if (!ctx.state.filesMap.has(id) || !existsSync(id))
85+
return
86+
return fs.writeFile(id, content, 'utf-8')
87+
},
88+
async saveSnapshotFile(id, content) {
89+
if (!ctx.snapshot.resolvedPaths.has(id))
90+
return
91+
await fs.mkdir(dirname(id), { recursive: true })
92+
return fs.writeFile(id, content, 'utf-8')
93+
},
94+
async removeSnapshotFile(id) {
95+
if (!ctx.snapshot.resolvedPaths.has(id) || !existsSync(id))
96+
return
97+
return fs.unlink(id)
98+
},
8399
snapshotSaved(snapshot) {
84100
ctx.snapshot.add(snapshot)
85101
},
86-
async writeFile(id, content, ensureDir) {
87-
if (ensureDir)
88-
await fs.mkdir(dirname(id), { recursive: true })
89-
return await fs.writeFile(id, content, 'utf-8')
90-
},
91102
async rerun(files) {
92103
await ctx.rerunFiles(files)
93104
},

‎packages/vitest/src/api/types.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ export interface WebSocketHandlers {
2121
resolveSnapshotRawPath(testPath: string, rawPath: string): string
2222
getModuleGraph(id: string): Promise<ModuleGraphData>
2323
getTransformResult(id: string): Promise<TransformResultWithSource | undefined>
24-
readFile(id: string): Promise<string | null>
25-
writeFile(id: string, content: string, ensureDir?: boolean): Promise<void>
26-
removeFile(id: string): Promise<void>
27-
createDirectory(id: string): Promise<string | undefined>
24+
readSnapshotFile(id: string): Promise<string | null>
25+
readTestFile(id: string): Promise<string | null>
26+
saveTestFile(id: string, content: string): Promise<void>
27+
saveSnapshotFile(id: string, content: string): Promise<void>
28+
removeSnapshotFile(id: string): Promise<void>
2829
snapshotSaved(snapshot: SnapshotResult): void
2930
rerun(files: string[]): Promise<void>
3031
updateSnapshot(file?: File): Promise<void>

‎packages/vitest/src/integrations/browser/server.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { resolveApiServerConfig } from '../../node/config'
88
import { CoverageTransform } from '../../node/plugins/coverageTransform'
99
import type { WorkspaceProject } from '../../node/workspace'
1010
import { MocksPlugin } from '../../node/plugins/mocks'
11+
import { resolveFsAllow } from '../../node/plugins/utils'
1112

1213
export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) {
1314
const root = project.config.root
@@ -44,7 +45,13 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us
4445

4546
config.server = server
4647
config.server.fs ??= {}
47-
config.server.fs.strict = false
48+
config.server.fs.allow = config.server.fs.allow || []
49+
config.server.fs.allow.push(
50+
...resolveFsAllow(
51+
project.ctx.config.root,
52+
project.ctx.server.config.configFile,
53+
),
54+
)
4855

4956
return {
5057
resolve: {

‎packages/vitest/src/node/create.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export async function createVitest(mode: VitestRunMode, options: UserConfig, vit
1717
? resolve(root, options.config)
1818
: await findUp(configFiles, { cwd: root } as any)
1919

20+
options.config = configPath
21+
2022
const config: ViteInlineConfig = {
2123
logLevel: 'error',
2224
configFile: configPath,

‎packages/vitest/src/node/plugins/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { GlobalSetupPlugin } from './globalSetup'
1212
import { CSSEnablerPlugin } from './cssEnabler'
1313
import { CoverageTransform } from './coverageTransform'
1414
import { MocksPlugin } from './mocks'
15-
import { deleteDefineConfig, hijackVitePluginInject, resolveOptimizerConfig } from './utils'
15+
import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow, resolveOptimizerConfig } from './utils'
1616
import { VitestResolver } from './vitestResolver'
1717

1818
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
8787
open,
8888
hmr: false,
8989
preTransformRequests: false,
90+
fs: {
91+
allow: resolveFsAllow(getRoot(), testConfig.config),
92+
},
9093
},
9194
}
9295

‎packages/vitest/src/node/plugins/utils.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { builtinModules } from 'node:module'
2-
import { version as viteVersion } from 'vite'
2+
import { searchForWorkspaceRoot, version as viteVersion } from 'vite'
33
import type { DepOptimizationOptions, ResolvedConfig, UserConfig as ViteConfig } from 'vite'
4+
import { dirname } from 'pathe'
45
import type { DepsOptimizationOptions, InlineConfig } from '../../types'
56

67
export function resolveOptimizerConfig(_testOptions: DepsOptimizationOptions | undefined, viteOptions: DepOptimizationOptions | undefined, testConfig: InlineConfig) {
@@ -84,3 +85,9 @@ export function hijackVitePluginInject(viteConfig: ResolvedConfig) {
8485
}
8586
}
8687
}
88+
89+
export function resolveFsAllow(projectRoot: string, rootConfigFile: string | false | undefined) {
90+
if (!rootConfigFile)
91+
return [searchForWorkspaceRoot(projectRoot)]
92+
return [dirname(rootConfigFile), searchForWorkspaceRoot(projectRoot)]
93+
}

‎packages/vitest/src/node/plugins/workspace.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { CSSEnablerPlugin } from './cssEnabler'
1010
import { SsrReplacerPlugin } from './ssrReplacer'
1111
import { GlobalSetupPlugin } from './globalSetup'
1212
import { MocksPlugin } from './mocks'
13-
import { deleteDefineConfig, hijackVitePluginInject, resolveOptimizerConfig } from './utils'
13+
import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow, resolveOptimizerConfig } from './utils'
1414
import { VitestResolver } from './vitestResolver'
1515

1616
interface WorkspaceOptions extends UserWorkspaceConfig {
@@ -69,6 +69,12 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp
6969
open: false,
7070
hmr: false,
7171
preTransformRequests: false,
72+
fs: {
73+
allow: resolveFsAllow(
74+
project.ctx.config.root,
75+
project.ctx.server.config.configFile,
76+
),
77+
},
7278
},
7379
test: {
7480
env,

‎pnpm-lock.yaml

+12-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/restricted/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@vitest/test-restricted",
3+
"private": true,
4+
"scripts": {
5+
"test": "vitest",
6+
"coverage": "vitest run --coverage"
7+
},
8+
"devDependencies": {
9+
"jsdom": "^22.1.0",
10+
"vitest": "workspace:*"
11+
}
12+
}

‎test/restricted/src/math.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function multiply(a, b) {
2+
return a * b
3+
}

‎test/restricted/tests/basic.spec.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { expect, it } from 'vitest'
2+
import { multiply } from '../src/math'
3+
4+
it('2 x 2 = 4', () => {
5+
expect(multiply(2, 2)).toBe(4)
6+
expect(multiply(2, 2)).toBe(Math.sqrt(16))
7+
})

‎test/restricted/vitest.config.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { resolve } from 'pathe'
2+
import { defineConfig } from 'vite'
3+
4+
export default defineConfig({
5+
plugins: [
6+
{
7+
// simulates restrictive FS
8+
name: 'restrict-fs',
9+
config() {
10+
return {
11+
server: {
12+
fs: {
13+
allow: [
14+
resolve(__dirname, 'src'),
15+
],
16+
},
17+
},
18+
}
19+
},
20+
},
21+
],
22+
test: {
23+
environment: 'jsdom',
24+
include: ['tests/**/*.spec.{js,ts}'],
25+
setupFiles: [
26+
'./vitest.setup.js',
27+
],
28+
},
29+
})

‎test/restricted/vitest.setup.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
globalThis.SOME_TEST_VARIABLE = '3'

0 commit comments

Comments
 (0)
Please sign in to comment.