@mintlify/cli 4.0.901 → 4.0.902

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ import { init } from '../src/init.js';
2
+
3
+ vi.mock('@inquirer/prompts', () => ({
4
+ select: vi.fn(),
5
+ input: vi.fn(),
6
+ }));
7
+
8
+ vi.mock('@mintlify/previewing', () => ({
9
+ addLogs: vi.fn(),
10
+ addLog: vi.fn(),
11
+ SpinnerLog: vi.fn(),
12
+ removeLastLog: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('@mintlify/validation', () => ({
16
+ docsConfigSchema: {
17
+ options: [{ shape: { theme: { _def: { value: 'quill' } } } }],
18
+ },
19
+ }));
20
+
21
+ vi.mock('fs-extra', () => ({
22
+ default: {
23
+ readdir: vi.fn().mockResolvedValue([]),
24
+ ensureDir: vi.fn().mockResolvedValue(undefined),
25
+ writeFile: vi.fn().mockResolvedValue(undefined),
26
+ copy: vi.fn().mockResolvedValue(undefined),
27
+ remove: vi.fn().mockResolvedValue(undefined),
28
+ readJson: vi.fn().mockResolvedValue({ theme: 'quill', name: 'Test' }),
29
+ writeJson: vi.fn().mockResolvedValue(undefined),
30
+ },
31
+ }));
32
+
33
+ vi.mock('adm-zip', () => ({
34
+ default: vi.fn().mockImplementation(() => ({
35
+ extractAllTo: vi.fn(),
36
+ })),
37
+ }));
38
+
39
+ global.fetch = vi.fn().mockResolvedValue({
40
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
41
+ });
42
+
43
+ describe('init', () => {
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ });
47
+
48
+ describe('path traversal prevention', () => {
49
+ it('rejects path traversal with ../', async () => {
50
+ await expect(init('../outside', false, 'quill', 'Test')).rejects.toThrow(
51
+ 'Access denied: path "../outside" is outside the current directory'
52
+ );
53
+ });
54
+
55
+ it('rejects deep path traversal', async () => {
56
+ await expect(init('../../../etc', false, 'quill', 'Test')).rejects.toThrow(
57
+ 'Access denied: path "../../../etc" is outside the current directory'
58
+ );
59
+ });
60
+
61
+ it('rejects absolute paths outside cwd', async () => {
62
+ await expect(init('/etc/test', false, 'quill', 'Test')).rejects.toThrow(
63
+ 'Access denied: path "/etc/test" is outside the current directory'
64
+ );
65
+ });
66
+
67
+ it('allows current directory (.)', async () => {
68
+ // Should not throw for current directory
69
+ await expect(init('.', false, 'quill', 'Test')).resolves.not.toThrow();
70
+ });
71
+
72
+ it('allows subdirectory paths', async () => {
73
+ // Should not throw for subdirectory
74
+ await expect(init('docs', false, 'quill', 'Test')).resolves.not.toThrow();
75
+ });
76
+
77
+ it('allows nested subdirectory paths', async () => {
78
+ // Should not throw for nested subdirectory
79
+ await expect(init('docs/api', false, 'quill', 'Test')).resolves.not.toThrow();
80
+ });
81
+ });
82
+ });
@@ -125,3 +125,20 @@ describe('openApiCheck', () => {
125
125
  expect(processExitMock).toHaveBeenCalledWith(1);
126
126
  });
127
127
  });
128
+
129
+ describe('readLocalOpenApiFile', () => {
130
+ it('rejects path traversal attempts', async () => {
131
+ const { readLocalOpenApiFile } =
132
+ await vi.importActual<typeof import('../src/helpers.js')>('../src/helpers.js');
133
+
134
+ await expect(readLocalOpenApiFile('../etc/passwd')).rejects.toThrow(
135
+ 'Access denied: invalid path'
136
+ );
137
+ await expect(readLocalOpenApiFile('/etc/passwd')).rejects.toThrow(
138
+ 'Access denied: invalid path'
139
+ );
140
+ await expect(readLocalOpenApiFile('../../secret.yaml')).rejects.toThrow(
141
+ 'Access denied: invalid path'
142
+ );
143
+ });
144
+ });
package/bin/helpers.js CHANGED
@@ -143,8 +143,14 @@ export const suppressConsoleWarnings = () => {
143
143
  };
144
144
  };
145
145
  export const readLocalOpenApiFile = (filename) => __awaiter(void 0, void 0, void 0, function* () {
146
- const pathname = path.resolve(process.cwd(), filename);
147
- const file = yield fs.readFile(pathname, 'utf-8');
146
+ const baseDir = process.cwd();
147
+ // const pathname = path.resolve(process.cwd(), filename);
148
+ const resolvedPath = path.resolve(baseDir, filename);
149
+ const relative = path.relative(baseDir, resolvedPath);
150
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
151
+ throw new Error('Access denied: invalid path');
152
+ }
153
+ const file = yield fs.readFile(resolvedPath, 'utf-8');
148
154
  const document = yaml.load(file);
149
155
  return document;
150
156
  });
package/bin/init.js CHANGED
@@ -14,6 +14,15 @@ import { docsConfigSchema } from '@mintlify/validation';
14
14
  import AdmZip from 'adm-zip';
15
15
  import fse from 'fs-extra';
16
16
  import { Box, Text } from 'ink';
17
+ import path from 'path';
18
+ const validatePathWithinCwd = (inputPath) => {
19
+ const baseDir = process.cwd();
20
+ const resolvedPath = path.resolve(baseDir, inputPath);
21
+ const relative = path.relative(baseDir, resolvedPath);
22
+ if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
23
+ throw new Error(`Access denied: path "${inputPath}" is outside the current directory`);
24
+ }
25
+ };
17
26
  const sendOnboardingMessage = (installDir) => {
18
27
  addLogs(_jsx(Text, { bold: true, children: "Documentation Setup!" }), _jsx(Text, { children: "To see your docs run" }), _jsxs(Box, { children: [_jsx(Text, { color: "blue", children: "cd" }), _jsxs(Text, { children: [" ", installDir] })] }), _jsx(Text, { color: "blue", children: "mint dev" }));
19
28
  };
@@ -22,6 +31,8 @@ const sendUsageMessageForAI = (directory, contentsOccupied, themes) => {
22
31
  };
23
32
  export function init(installDir, force, theme, name) {
24
33
  return __awaiter(this, void 0, void 0, function* () {
34
+ // Validate path is within current working directory to prevent path traversal
35
+ validatePathWithinCwd(installDir);
25
36
  const isInteractive = process.stdin.isTTY;
26
37
  const isClaudeCode = process.env.CLAUDECODE === '1';
27
38
  const isAI = !isInteractive || isClaudeCode;
@@ -62,6 +73,8 @@ export function init(installDir, force, theme, name) {
62
73
  throw new Error('Subdirectory name cannot be empty');
63
74
  }
64
75
  installDir = installDir === '.' ? subdir : `${installDir}/${subdir}`;
76
+ // Re-validate after subdirectory is appended
77
+ validatePathWithinCwd(installDir);
65
78
  }
66
79
  }
67
80
  if (!isAI && (!selectedTheme || !projectName)) {