Skip to content

Commit bcb41e5

Browse files
authored
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

Lines changed: 3 additions & 7 deletions
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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 0 additions & 10 deletions
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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 10 additions & 5 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 23 additions & 12 deletions
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

Lines changed: 5 additions & 4 deletions
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

Lines changed: 8 additions & 1 deletion
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: {

0 commit comments

Comments
 (0)