@@ -4,7 +4,6 @@ import { spawn } from 'child_process';
44import { MockGitLabServer , findMockServerPort } from './utils/mock-gitlab-server.js' ;
55import fs from 'node:fs' ;
66import path from 'node:path' ;
7- import os from 'node:os' ;
87
98const MOCK_TOKEN = 'glpat-mock-token-12345' ;
109const TEST_PROJECT_ID = '123' ;
@@ -143,8 +142,12 @@ describe('job artifacts tools', () => {
143142 await mockGitLab . start ( ) ;
144143 mockGitLabUrl = mockGitLab . getUrl ( ) ;
145144
146- // Create a temp directory for download tests
147- tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'gitlab-mcp-test-' ) ) ;
145+ // Create a temp directory for download tests. Must be relative to the
146+ // process cwd — download_job_artifacts rejects absolute local_path
147+ // values as directory traversal (see downloadJobArtifacts/downloadAttachment).
148+ tmpDir = `gitlab-mcp-test-artifacts-${ process . pid } ` ;
149+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
150+ fs . mkdirSync ( tmpDir , { recursive : true } ) ;
148151 } ) ;
149152
150153 after ( async ( ) => {
@@ -210,6 +213,44 @@ describe('job artifacts tools', () => {
210213 assert . ok ( fs . existsSync ( nestedLocalPath ) , `Directory should be created at ${ nestedLocalPath } ` ) ;
211214 } ) ;
212215
216+ test ( 'download_job_artifacts rejects local_path directory traversal' , async ( ) => {
217+ try {
218+ await callTool (
219+ 'download_job_artifacts' ,
220+ { project_id : TEST_PROJECT_ID , job_id : TEST_JOB_ID , local_path : '../../../tmp' } ,
221+ {
222+ GITLAB_API_URL : `${ mockGitLabUrl } /api/v4` ,
223+ GITLAB_PERSONAL_ACCESS_TOKEN : MOCK_TOKEN ,
224+ }
225+ ) ;
226+ assert . fail ( 'Expected download_job_artifacts to reject a traversal local_path' ) ;
227+ } catch ( error : any ) {
228+ assert . ok (
229+ typeof error ?. message === 'string' && error . message . toLowerCase ( ) . includes ( 'traversal' ) ,
230+ `Expected a traversal error, got: ${ error ?. message } `
231+ ) ;
232+ }
233+ } ) ;
234+
235+ test ( 'download_job_artifacts rejects absolute local_path' , async ( ) => {
236+ try {
237+ await callTool (
238+ 'download_job_artifacts' ,
239+ { project_id : TEST_PROJECT_ID , job_id : TEST_JOB_ID , local_path : '/tmp' } ,
240+ {
241+ GITLAB_API_URL : `${ mockGitLabUrl } /api/v4` ,
242+ GITLAB_PERSONAL_ACCESS_TOKEN : MOCK_TOKEN ,
243+ }
244+ ) ;
245+ assert . fail ( 'Expected download_job_artifacts to reject an absolute local_path' ) ;
246+ } catch ( error : any ) {
247+ assert . ok (
248+ typeof error ?. message === 'string' && error . message . toLowerCase ( ) . includes ( 'traversal' ) ,
249+ `Expected a traversal error, got: ${ error ?. message } `
250+ ) ;
251+ }
252+ } ) ;
253+
213254 test ( 'get_job_artifact_file returns file content' , async ( ) => {
214255 const result = await callTool (
215256 'get_job_artifact_file' ,
0 commit comments