2727import io .kubernetes .client .openapi .models .V1Pod ;
2828import io .kubernetes .client .util .ClientBuilder ;
2929import io .kubernetes .client .util .exception .CopyNotSupportedException ;
30+ import java .io .ByteArrayInputStream ;
3031import java .io .IOException ;
3132import java .io .InputStream ;
33+ import java .io .OutputStream ;
3234import java .nio .file .Files ;
3335import java .nio .file .Path ;
3436import java .nio .file .Paths ;
37+ import java .util .HashMap ;
38+ import java .util .Map ;
3539import org .junit .jupiter .api .BeforeEach ;
3640import org .junit .jupiter .api .Test ;
3741import org .junit .jupiter .api .extension .RegisterExtension ;
3842import org .junit .jupiter .api .io .TempDir ;
3943
44+ import static org .junit .jupiter .api .Assertions .assertFalse ;
45+ import static org .junit .jupiter .api .Assertions .assertEquals ;
46+ import static org .junit .jupiter .api .Assertions .assertThrows ;
47+ import static org .junit .jupiter .api .Assertions .assertTrue ;
48+
4049/** Tests for the Copy helper class */
4150class CopyTest {
4251 private String namespace ;
@@ -45,6 +54,71 @@ class CopyTest {
4554
4655 private ApiClient client ;
4756
57+ private static class MockProcess extends Process {
58+ private final InputStream inputStream ;
59+
60+ MockProcess (String stdout ) {
61+ this .inputStream = new ByteArrayInputStream (stdout .getBytes ());
62+ }
63+
64+ @ Override
65+ public OutputStream getOutputStream () {
66+ return OutputStream .nullOutputStream ();
67+ }
68+
69+ @ Override
70+ public InputStream getInputStream () {
71+ return inputStream ;
72+ }
73+
74+ @ Override
75+ public InputStream getErrorStream () {
76+ return InputStream .nullInputStream ();
77+ }
78+
79+ @ Override
80+ public int waitFor () {
81+ return 0 ;
82+ }
83+
84+ @ Override
85+ public int exitValue () {
86+ return 0 ;
87+ }
88+
89+ @ Override
90+ public void destroy () {
91+ // no-op for tests
92+ }
93+ }
94+
95+ private static class MockCopy extends Copy {
96+ private final Map <String , String > listingsByPath ;
97+
98+ MockCopy (Map <String , String > listingsByPath ) {
99+ this .listingsByPath = listingsByPath ;
100+ }
101+
102+ @ Override
103+ public Process exec (
104+ String namespace ,
105+ String name ,
106+ String [] command ,
107+ String container ,
108+ boolean stdin ,
109+ boolean tty ) {
110+ String cmd = command [2 ];
111+ String srcPath = cmd .substring ("ls -F " .length ());
112+ return new MockProcess (listingsByPath .getOrDefault (srcPath , "" ));
113+ }
114+
115+ @ Override
116+ public InputStream copyFileFromPod (String namespace , String pod , String srcPath )
117+ throws ApiException , IOException {
118+ return new ByteArrayInputStream ("test-data" .getBytes ());
119+ }
120+ }
121+
48122 @ RegisterExtension
49123 static WireMockExtension apiServer =
50124 WireMockExtension .newInstance ().options (wireMockConfig ().dynamicPort ()).build ();
@@ -225,4 +299,53 @@ public void run() {
225299 .withQueryParam ("command" , equalTo ("-c" ))
226300 .withQueryParam ("command" , equalTo ("tar --version" )));
227301 }
302+
303+ @ Test
304+ void rejectsPathTraversalInNonTarCopy (@ TempDir Path tempDir ) throws Exception {
305+ Path sourceRoot = Paths .get ("/src" );
306+ Path destinationRoot = tempDir .resolve ("dest" );
307+ Path escapedFile = tempDir .resolve ("escape.txt" );
308+
309+ Map <String , String > listingsByPath = new HashMap <>();
310+ listingsByPath .put (sourceRoot .toString (), "../escape.txt\n " );
311+
312+ Copy copy = new MockCopy (listingsByPath );
313+
314+ assertThrows (
315+ IOException .class ,
316+ () ->
317+ copy .copyDirectoryFromPod (
318+ namespace ,
319+ podName ,
320+ null ,
321+ sourceRoot .toString (),
322+ destinationRoot ,
323+ false ));
324+
325+ assertFalse (Files .exists (escapedFile ));
326+ }
327+
328+ @ Test
329+ void copiesSafeEntriesInNonTarMode (@ TempDir Path tempDir ) throws Exception {
330+ Path sourceRoot = Paths .get ("/src" );
331+ Path destinationRoot = tempDir .resolve ("dest" );
332+ Files .createDirectories (destinationRoot );
333+
334+ Map <String , String > listingsByPath = new HashMap <>();
335+ listingsByPath .put (sourceRoot .toString (), "safe.txt\n " );
336+
337+ Copy copy = new MockCopy (listingsByPath );
338+
339+ copy .copyDirectoryFromPod (
340+ namespace ,
341+ podName ,
342+ null ,
343+ sourceRoot .toString (),
344+ destinationRoot ,
345+ false );
346+
347+ Path copied = destinationRoot .resolve ("safe.txt" );
348+ assertTrue (Files .exists (copied ));
349+ assertEquals ("test-data" , Files .readString (copied ));
350+ }
228351}
0 commit comments