Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: restrict access to file system via API #3956

Merged
merged 8 commits into from Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 3 additions & 7 deletions packages/browser/src/client/snapshot.ts
Expand Up @@ -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> {
Expand All @@ -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)
}
}
5 changes: 4 additions & 1 deletion packages/snapshot/src/manager.ts
Expand Up @@ -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'>) {
Expand All @@ -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) {
Expand Down
10 changes: 0 additions & 10 deletions packages/snapshot/src/port/utils.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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')
}
Expand All @@ -157,7 +149,6 @@ export async function saveSnapshotFile(
if (skipWriting)
return

await ensureDirectoryExists(environment, snapshotPath)
await environment.saveSnapshotFile(
snapshotPath,
content,
Expand All @@ -175,7 +166,6 @@ export async function saveSnapshotFileRaw(
if (skipWriting)
return

await ensureDirectoryExists(environment, snapshotPath)
await environment.saveSnapshotFile(
snapshotPath,
content,
Expand Down
1 change: 0 additions & 1 deletion packages/snapshot/src/types/environment.ts
Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/client/components/views/ViewEditor.vue
Expand Up @@ -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
},
Expand Down Expand Up @@ -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
}
Expand Down
15 changes: 10 additions & 5 deletions packages/ui/client/composables/client/static.ts
Expand Up @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/src/source-map.ts
Expand Up @@ -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,
Expand Down
35 changes: 23 additions & 12 deletions packages/vitest/src/api/setup.ts
Expand Up @@ -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)
},
createDirectory(id) {
return fs.mkdir(id, { recursive: true })
async readSnapshotFile(snapshotPath) {
if (!ctx.snapshot.resolvedPaths.has(snapshotPath) || !existsSync(snapshotPath))
return null
return fs.readFile(snapshotPath, 'utf-8')
},
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)
},
Expand Down
9 changes: 5 additions & 4 deletions packages/vitest/src/api/types.ts
Expand Up @@ -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>
Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/src/integrations/browser/server.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions packages/vitest/src/node/create.ts
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/node/plugins/index.ts
Expand Up @@ -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[]> {
Expand Down Expand Up @@ -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),
},
},
}

Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/src/node/plugins/utils.ts
@@ -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) {
Expand Down Expand Up @@ -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)]
}
8 changes: 7 additions & 1 deletion packages/vitest/src/node/plugins/workspace.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions test/restricted/package.json
@@ -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 changes: 3 additions & 0 deletions test/restricted/src/math.js
@@ -0,0 +1,3 @@
export function multiply(a, b) {
return a * b
}
7 changes: 7 additions & 0 deletions test/restricted/tests/basic.spec.js
@@ -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))
})