Skip to content

Commit 56c29e7

Browse files
committed
CVE-2026-62266 Path traversal in maintenance archive browser
1 parent 886e7e4 commit 56c29e7

2 files changed

Lines changed: 459 additions & 1 deletion

File tree

openidm-maintenance/src/main/java/org/forgerock/openidm/maintenance/impl/UpdateArchiveService.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@
1212
* information: "Portions copyright [year] [name of copyright owner]".
1313
*
1414
* Copyright 2016 ForgeRock AS.
15+
* Portions copyright 2024-2026 3A Systems LLC
1516
*/
1617
package org.forgerock.openidm.maintenance.impl;
1718

19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.nio.file.InvalidPathException;
1822
import java.nio.file.Path;
1923
import java.nio.file.Paths;
2024
import java.util.Iterator;
2125

2226
import org.forgerock.json.JsonValue;
2327
import org.forgerock.json.resource.AbstractRequestHandler;
28+
import org.forgerock.json.resource.BadRequestException;
2429
import org.forgerock.json.resource.InternalServerErrorException;
2530
import org.forgerock.json.resource.ReadRequest;
2631
import org.forgerock.json.resource.RequestHandler;
@@ -57,16 +62,58 @@ public class UpdateArchiveService extends AbstractRequestHandler {
5762
@Reference(policy= ReferencePolicy.STATIC)
5863
private UpdateManager updateManager;
5964

65+
File getInstallLocation() {
66+
return IdentityServer.getInstance().getInstallLocation();
67+
}
68+
6069
@Override
6170
public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
6271
final String archiveName = request.getResourcePathObject().get(0);
63-
final Path archivePath = IdentityServer.getInstance().getInstallLocation().toPath().resolve("bin/update").resolve(archiveName);
72+
// Resolve the real path of the allowed directory (collapses symlinks + "..")
73+
final Path updateBaseDir;
74+
try {
75+
updateBaseDir = getInstallLocation().toPath()
76+
.resolve("bin/update")
77+
.toRealPath(); // throws IOException if directory absent
78+
} catch (IOException e) {
79+
return new InternalServerErrorException("Unable to resolve update directory", e).asPromise();
80+
}
81+
82+
// Normalize the resolved archive path, then enforce prefix
83+
final Path archivePath;
84+
try {
85+
archivePath = updateBaseDir.resolve(archiveName).normalize();
86+
} catch (InvalidPathException e) {
87+
return new BadRequestException("Invalid archive path", e).asPromise();
88+
}
89+
if (!archivePath.startsWith(updateBaseDir)) {
90+
return new BadRequestException("Invalid archive path").asPromise();
91+
}
92+
93+
// Defense in depth: resolve symlinks on the archive itself, then re-verify containment.
94+
try {
95+
final Path realArchivePath = archivePath.toRealPath();
96+
if (!realArchivePath.startsWith(updateBaseDir)) {
97+
return new BadRequestException("Invalid archive path").asPromise();
98+
}
99+
} catch (IOException e) {
100+
return new BadRequestException("Invalid archive path", e).asPromise();
101+
}
102+
64103
Path requestedFile = Paths.get("");
65104
Iterator<String> it = request.getResourcePathObject().tail(1).iterator();
66105
while (it.hasNext()) {
67106
requestedFile = requestedFile.resolve(it.next());
68107
}
69108

109+
// Normalize to collapse any ".." sequences embedded in the segments
110+
requestedFile = requestedFile.normalize();
111+
112+
// Reject absolute paths or paths that start with ".." after normalization
113+
if (requestedFile.isAbsolute() || requestedFile.startsWith("..")) {
114+
return new BadRequestException("Invalid file path within archive").asPromise();
115+
}
116+
70117
try {
71118
final JsonValue fileContents = updateManager.getArchiveFile(archivePath, requestedFile);
72119
return Responses.newResourceResponse(null, null, fileContents).asPromise();

0 commit comments

Comments
 (0)