|
23 | 23 | import static com.github.tomakehurst.wiremock.client.WireMock.*; |
24 | 24 | import static org.junit.jupiter.api.Assertions.assertEquals; |
25 | 25 | import static org.junit.jupiter.api.Assertions.assertFalse; |
| 26 | +import static org.junit.jupiter.api.Assertions.assertInstanceOf; |
26 | 27 | import static org.junit.jupiter.api.Assertions.assertNotNull; |
27 | 28 | import static org.junit.jupiter.api.Assertions.assertThrows; |
| 29 | +import static org.junit.jupiter.api.Assertions.assertTrue; |
28 | 30 |
|
29 | 31 | import com.github.tomakehurst.wiremock.WireMockServer; |
30 | 32 | import com.github.tomakehurst.wiremock.client.WireMock; |
@@ -1072,4 +1074,76 @@ private byte[] tokenScenario( |
1072 | 1074 | ContainerRef.parse("localhost:%d/library/%s".formatted(wmRuntimeInfo.getHttpPort(), registryName)); |
1073 | 1075 | return registry.getBlob(containerRef.withDigest(digest)); |
1074 | 1076 | } |
| 1077 | + |
| 1078 | + @Test |
| 1079 | + void pullArtifactShouldRejectInvalidTitleAnnotation(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { |
| 1080 | + WireMock wireMock = wmRuntimeInfo.getWireMock(); |
| 1081 | + String registryUrl = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); |
| 1082 | + |
| 1083 | + // Craft a blob and build a manifest whose layer title contains a invalid sequence |
| 1084 | + byte[] blobContent = "malicious content".getBytes(java.nio.charset.StandardCharsets.UTF_8); |
| 1085 | + String blobDigest = SupportedAlgorithm.SHA256.digest(blobContent); |
| 1086 | + |
| 1087 | + Layer maliciousLayer = Layer.fromDigest(blobDigest, blobContent.length) |
| 1088 | + .withAnnotations(Map.of(Const.ANNOTATION_TITLE, "../traversed-file.txt")); |
| 1089 | + |
| 1090 | + Manifest manifest = Manifest.empty().withLayers(List.of(maliciousLayer)); |
| 1091 | + String manifestJson = JsonUtils.toJson(manifest); |
| 1092 | + String manifestDigest = |
| 1093 | + SupportedAlgorithm.SHA256.digest(manifestJson.getBytes(java.nio.charset.StandardCharsets.UTF_8)); |
| 1094 | + |
| 1095 | + // Stub HEAD manifest |
| 1096 | + wireMock.register(head(urlEqualTo("/v2/library/malicious-artifact/manifests/latest")) |
| 1097 | + .willReturn(aResponse() |
| 1098 | + .withStatus(200) |
| 1099 | + .withHeader(Const.CONTENT_TYPE_HEADER, Const.DEFAULT_MANIFEST_MEDIA_TYPE) |
| 1100 | + .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, manifestDigest))); |
| 1101 | + |
| 1102 | + // Stub GET manifest |
| 1103 | + wireMock.register(get(urlEqualTo("/v2/library/malicious-artifact/manifests/latest")) |
| 1104 | + .willReturn(aResponse() |
| 1105 | + .withStatus(200) |
| 1106 | + .withHeader(Const.CONTENT_TYPE_HEADER, Const.DEFAULT_MANIFEST_MEDIA_TYPE) |
| 1107 | + .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, manifestDigest) |
| 1108 | + .withBody(manifestJson))); |
| 1109 | + |
| 1110 | + // Stub GET blob |
| 1111 | + wireMock.register(get(urlEqualTo("/v2/library/malicious-artifact/blobs/%s".formatted(blobDigest))) |
| 1112 | + .willReturn(aResponse() |
| 1113 | + .withStatus(200) |
| 1114 | + .withBody(blobContent) |
| 1115 | + .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, blobDigest))); |
| 1116 | + |
| 1117 | + Registry registry = Registry.Builder.builder() |
| 1118 | + .withAuthProvider(authProvider) |
| 1119 | + .withInsecure(true) |
| 1120 | + .build(); |
| 1121 | + |
| 1122 | + ContainerRef containerRef = ContainerRef.parse("%s/library/malicious-artifact:latest".formatted(registryUrl)); |
| 1123 | + |
| 1124 | + Path outputDir = configDir.resolve("pull-output"); |
| 1125 | + Files.createDirectories(outputDir); |
| 1126 | + |
| 1127 | + // Check exception |
| 1128 | + Throwable cause = assertThrows( |
| 1129 | + Exception.class, |
| 1130 | + () -> registry.pullArtifact(containerRef, outputDir, true), |
| 1131 | + "Expected an exception for title annotation"); |
| 1132 | + while (cause.getCause() != null && !(cause instanceof OrasException)) { |
| 1133 | + cause = cause.getCause(); |
| 1134 | + } |
| 1135 | + assertInstanceOf( |
| 1136 | + OrasException.class, |
| 1137 | + cause, |
| 1138 | + "Root cause should be OrasException but was: " |
| 1139 | + + cause.getClass().getName()); |
| 1140 | + assertTrue( |
| 1141 | + cause.getMessage().contains("is not withing folder"), |
| 1142 | + "Exception message should mention is not withing folder but was: " + cause.getMessage()); |
| 1143 | + |
| 1144 | + // The file must NOT have been written outside the output directory |
| 1145 | + assertFalse( |
| 1146 | + Files.exists(outputDir.getParent().resolve("traversed-file.txt")), |
| 1147 | + "Blob must not be written outside the output directory"); |
| 1148 | + } |
1075 | 1149 | } |
0 commit comments