11import { afterEach , beforeEach , describe , expect , it } from "bun:test" ;
2- import { mkdirSync , rmSync , utimesSync , writeFileSync } from "node:fs" ;
2+ import { existsSync , mkdirSync , rmSync , utimesSync , writeFileSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
55import { CACHE_TTL_MS , getCacheDir , getCacheKey , readCache , writeCache } from "./cache.ts" ;
@@ -47,7 +47,10 @@ describe("getCacheDir", () => {
4747 const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" ) ;
4848 const originalLocalAppData = process . env . LOCALAPPDATA ;
4949 try {
50- Object . defineProperty ( process , "platform" , { value : "win32" , configurable : true } ) ;
50+ Object . defineProperty ( process , "platform" , {
51+ value : "win32" ,
52+ configurable : true ,
53+ } ) ;
5154 process . env . LOCALAPPDATA = "C:\\Users\\user\\AppData\\Local" ;
5255 const dir = getCacheDir ( ) ;
5356 // path.join uses the host OS separator in tests (macOS: /), so we just
@@ -67,7 +70,10 @@ describe("getCacheDir", () => {
6770 const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" ) ;
6871 const originalLocalAppData = process . env . LOCALAPPDATA ;
6972 try {
70- Object . defineProperty ( process , "platform" , { value : "win32" , configurable : true } ) ;
73+ Object . defineProperty ( process , "platform" , {
74+ value : "win32" ,
75+ configurable : true ,
76+ } ) ;
7177 delete process . env . LOCALAPPDATA ;
7278 const dir = getCacheDir ( ) ;
7379 expect ( dir ) . toContain ( "AppData" ) ;
@@ -84,7 +90,10 @@ describe("getCacheDir", () => {
8490 const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" ) ;
8591 const originalXdg = process . env . XDG_CACHE_HOME ;
8692 try {
87- Object . defineProperty ( process , "platform" , { value : "linux" , configurable : true } ) ;
93+ Object . defineProperty ( process , "platform" , {
94+ value : "linux" ,
95+ configurable : true ,
96+ } ) ;
8897 process . env . XDG_CACHE_HOME = "/custom/xdg/cache" ;
8998 const dir = getCacheDir ( ) ;
9099 expect ( dir ) . toContain ( "custom" ) ;
@@ -102,7 +111,10 @@ describe("getCacheDir", () => {
102111 const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" ) ;
103112 const originalXdg = process . env . XDG_CACHE_HOME ;
104113 try {
105- Object . defineProperty ( process , "platform" , { value : "linux" , configurable : true } ) ;
114+ Object . defineProperty ( process , "platform" , {
115+ value : "linux" ,
116+ configurable : true ,
117+ } ) ;
106118 delete process . env . XDG_CACHE_HOME ;
107119 const dir = getCacheDir ( ) ;
108120 expect ( dir ) . toContain ( ".cache" ) ;
@@ -117,7 +129,10 @@ describe("getCacheDir", () => {
117129 delete process . env . GITHUB_CODE_SEARCH_CACHE_DIR ;
118130 const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" ) ;
119131 try {
120- Object . defineProperty ( process , "platform" , { value : "freebsd" , configurable : true } ) ;
132+ Object . defineProperty ( process , "platform" , {
133+ value : "freebsd" ,
134+ configurable : true ,
135+ } ) ;
121136 const dir = getCacheDir ( ) ;
122137 expect ( dir ) . toContain ( ".github-code-search" ) ;
123138 expect ( dir ) . toContain ( "cache" ) ;
@@ -224,3 +239,60 @@ describe("writeCache / readCache round-trip", () => {
224239 expect ( ( ) => writeCache ( "key.json" , { data : 1 } ) ) . not . toThrow ( ) ;
225240 } ) ;
226241} ) ;
242+
243+ // ─── Path Traversal Security Tests ────────────────────────────────────────────
244+
245+ describe ( "Path traversal vulnerability mitigation" , ( ) => {
246+ it ( "rejects readCache with relative parent directory traversal (../)" , ( ) => {
247+ // Attempt to read outside the cache directory using ../
248+ const maliciousKey = "../../../etc/passwd" ;
249+ const result = readCache ( maliciousKey ) ;
250+ expect ( result ) . toBeNull ( ) ;
251+ } ) ;
252+
253+ it ( "rejects writeCache with relative parent directory traversal (../)" , ( ) => {
254+ // Use a subdirectory as cache dir so we have a writable location just above it
255+ const cacheSubDir = join ( TEST_CACHE_DIR , "sub" ) ;
256+ mkdirSync ( cacheSubDir , { recursive : true } ) ;
257+ process . env . GITHUB_CODE_SEARCH_CACHE_DIR = cacheSubDir ;
258+ // This key resolves to TEST_CACHE_DIR/escaped.json — one level above cacheSubDir
259+ const maliciousKey = "../escaped.json" ;
260+ const outsidePath = join ( TEST_CACHE_DIR , "escaped.json" ) ;
261+ expect ( ( ) => writeCache ( maliciousKey , { exploit : true } ) ) . not . toThrow ( ) ;
262+ // Prove the file was NOT created at the resolved outside location
263+ expect ( existsSync ( outsidePath ) ) . toBe ( false ) ;
264+ } ) ;
265+
266+ it ( "rejects readCache with absolute path" , ( ) => {
267+ // Attempt to read an absolute path outside the cache directory
268+ // Use a portable OS temp path that exists on all platforms
269+ const maliciousKey = join ( tmpdir ( ) , "gcs-read-absolute-test.json" ) ;
270+ const result = readCache ( maliciousKey ) ;
271+ expect ( result ) . toBeNull ( ) ;
272+ } ) ;
273+
274+ it ( "rejects writeCache with absolute path" , ( ) => {
275+ // Attempt to write to an absolute path outside the cache directory
276+ const outsidePath = join ( tmpdir ( ) , `gcs-write-absolute-test-${ process . pid } .json` ) ;
277+ expect ( ( ) => writeCache ( outsidePath , { exploit : "absolute" } ) ) . not . toThrow ( ) ;
278+ // Prove the file was NOT created at the absolute outside location
279+ expect ( existsSync ( outsidePath ) ) . toBe ( false ) ;
280+ } ) ;
281+
282+ it ( "rejects readCache with encoded path traversal (%2e%2e/)" , ( ) => {
283+ // URL-encoded path traversal attempt
284+ const maliciousKey = "%2e%2e/%2e%2e/etc/passwd" ;
285+ const result = readCache ( maliciousKey ) ;
286+ // The key is treated as a literal filename, not decoded, so it's safe
287+ // but should still not exist
288+ expect ( result ) . toBeNull ( ) ;
289+ } ) ;
290+
291+ it ( "allows reading legitimate cache files with safe names" , ( ) => {
292+ // Verify that normal operation still works
293+ const safeKey = "teams__myorg__squad-.json" ;
294+ const data = { teams : [ "squad-alpha" ] } ;
295+ writeCache ( safeKey , data ) ;
296+ expect ( readCache ( safeKey ) ) . toEqual ( data ) ;
297+ } ) ;
298+ } ) ;
0 commit comments