Skip to content

Commit

Permalink
fix: restrict access to file system via API (#3956)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Aug 15, 2023
1 parent 91fe485 commit bcb41e5
Show file tree
Hide file tree
Showing 20 changed files with 143 additions and 49 deletions.
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))
})

0 comments on commit bcb41e5

Please sign in to comment.