Skip to content

Commit cc677c4

Browse files
orbisai0securityclaudeantfu
authored
fix: remove unsafe exec() in resolver.ts (#2585)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 439ba7a commit cc677c4

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

packages/slidev/node/resolver.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,79 @@ describe('createResolver', () => {
6161
],
6262
)
6363
})
64+
65+
it('resolves valid scoped name through validation', async () => {
66+
const resolver = createResolver('theme', {})
67+
68+
const res = await resolver('@slidev/theme-official', '/')
69+
70+
expect(res).toEqual([
71+
'@slidev/theme-official',
72+
'/user/project/node_modules/@slidev/theme-official',
73+
])
74+
})
75+
76+
it('passes through "none" without validation', async () => {
77+
const resolver = createResolver('theme', {})
78+
79+
const res = await resolver('none', '/')
80+
81+
expect(res).toEqual(['', null])
82+
})
83+
84+
it('rejects names with shell metacharacters', async () => {
85+
const resolver = createResolver('theme', {})
86+
87+
await expect(resolver('evil$(rm -rf)', '/')).rejects.toThrowError(
88+
'Invalid theme name "evil$(rm -rf)". Only valid npm package names are allowed.',
89+
)
90+
})
91+
92+
it('rejects names with backslashes', async () => {
93+
const resolver = createResolver('theme', {})
94+
95+
await expect(resolver('evil\\package', '/')).rejects.toThrowError(
96+
/Invalid theme name/,
97+
)
98+
})
99+
100+
it('rejects names with uppercase letters', async () => {
101+
const resolver = createResolver('theme', {})
102+
103+
await expect(resolver('EvilTheme', '/')).rejects.toThrowError(
104+
/Invalid theme name/,
105+
)
106+
})
107+
108+
it('rejects scoped names with extra slash segments', async () => {
109+
const resolver = createResolver('theme', {})
110+
111+
await expect(resolver('@scope/foo/bar', '/')).rejects.toThrowError(
112+
/Invalid theme name/,
113+
)
114+
})
115+
116+
it('rejects scoped names with parent traversal segment', async () => {
117+
const resolver = createResolver('theme', {})
118+
119+
await expect(resolver('@evil/../escape', '/')).rejects.toThrowError(
120+
/Invalid theme name/,
121+
)
122+
})
123+
124+
it('rejects empty string', async () => {
125+
const resolver = createResolver('theme', {})
126+
127+
await expect(resolver('', '/')).rejects.toThrowError(
128+
/Invalid theme name/,
129+
)
130+
})
131+
132+
it('reflects the resolver type in the error message', async () => {
133+
const resolver = createResolver('addon', {})
134+
135+
await expect(resolver('evil$(rm -rf)', '/')).rejects.toThrowError(
136+
'Invalid addon name "evil$(rm -rf)". Only valid npm package names are allowed.',
137+
)
138+
})
64139
})

packages/slidev/node/resolver.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { resolveGlobal } from 'resolve-global'
1414
import { findClosestPkgJsonPath, findDepPkgJsonPath } from 'vitefu'
1515

1616
const RE_PATH_SEPARATOR = /[/\\]/
17+
const RE_SAFE_PKG_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
1718

1819
const cliRoot = fileURLToPath(new URL('..', import.meta.url))
1920

@@ -148,6 +149,10 @@ export function createResolver(type: 'theme' | 'addon', officials: Record<string
148149
if (name[0] === '.' || (name[0] !== '@' && name.includes('/')))
149150
return [name, resolve(dirname(importer), name)]
150151

152+
// Validate that the name is a safe npm package name before resolving
153+
if (!RE_SAFE_PKG_NAME.test(name))
154+
throw new Error(`Invalid ${type} name "${name}". Only valid npm package names are allowed.`)
155+
151156
// search for local packages first
152157
{
153158
const possiblePkgNames = [name]

0 commit comments

Comments
 (0)