Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* ====================
*
* Portions Copyrighted 2012 ForgeRock AS
* Portions Copyrighted 2026 3A Systems, LLC
*
*/
package org.identityconnectors.contract.test;
Expand Down Expand Up @@ -526,7 +527,7 @@ private void sleepIngoringInterruption(long sleepTime) {
private boolean authenticateExpectingRuntimeException(ObjectClass objectClass, String name, GuardedString password) {
boolean authenticateFailed = false;

for(int i=0;i<getLongTestParam(MAX_ITERATIONS, 1);i++) {
for (long i = 0; i < getLongTestParam(MAX_ITERATIONS, 1); i++) {
try {
getConnectorFacade().authenticate(ObjectClass.ACCOUNT, name,password,
getOperationOptionsByOp(objectClass, AuthenticationApiOp.class));
Expand All @@ -545,7 +546,7 @@ private boolean authenticateExpectingRuntimeException(ObjectClass objectClass, S
private boolean authenticateExpectingInvalidCredentials(ObjectClass objectClass, String name, GuardedString password) {
boolean authenticateFailed = false;

for(int i=0;i<getLongTestParam(MAX_ITERATIONS, 1);i++) {
for (long i = 0; i < getLongTestParam(MAX_ITERATIONS, 1); i++) {
try {
getConnectorFacade().authenticate(ObjectClass.ACCOUNT, name, password,
getOperationOptionsByOp(objectClass, AuthenticationApiOp.class));
Expand All @@ -565,7 +566,7 @@ private Uid authenticateExpectingSuccess(ObjectClass objectClass, String name, G
Uid authenticatedUid = null;
RuntimeException lastException = null;

for(int i=0;i<getLongTestParam(MAX_ITERATIONS, 1);i++) {
for (long i = 0; i < getLongTestParam(MAX_ITERATIONS, 1); i++) {
try {
authenticatedUid = getConnectorFacade().authenticate(ObjectClass.ACCOUNT, name,password,
getOperationOptionsByOp(objectClass, AuthenticationApiOp.class));
Expand All @@ -589,7 +590,7 @@ private PasswordExpiredException authenticateExpectingPasswordExpired(ObjectClas
PasswordExpiredException passwordExpiredException = null;
RuntimeException lastException = null;

for(int i=0;i<getLongTestParam(MAX_ITERATIONS, 1);i++) {
for (long i = 0; i < getLongTestParam(MAX_ITERATIONS, 1); i++) {
try {
getConnectorFacade().authenticate(ObjectClass.ACCOUNT, name,password,
getOperationOptionsByOp(objectClass, AuthenticationApiOp.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* ====================
* Portions Copyrighted 2010-2015 ForgeRock AS.
* Portions Copyrighted 2010-2014 Tirasa.
* Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.impl.api.local;

Expand Down Expand Up @@ -454,8 +455,12 @@ public File copyStreamToFile(final InputStream stream) throws IOException {

public File copyStreamToFile(final InputStream stream, final String name)
throws IOException {
final File bundleDir = getBundleTempDir();
final File newFile = new File(bundleDir, name);
// canonical, like the file resolveEntry returns, so that the
// parent walk below ends at bundleDir even when java.io.tmpdir
// goes through a symbolic link
final File bundleDir = getBundleTempDir().getCanonicalFile();
// refuses entries such as lib/../../x that would leave bundleDir
final File newFile = IOUtil.resolveEntry(bundleDir, name);
if (newFile.exists()) {
throw new IOException("File " + newFile + " already exists");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ private PooledObject borrowObjectNoTest() throws InterruptedException {
final ReentrantLock lock = this.takeLock;
lock.lockInterruptibly();
try {
do {
// leaves only by returning an object or by throwing: on the
// timeout below, or when interrupted
while (true) {
if (totalPermit.tryAcquire()) {
// If the pool is empty and there are available permits
// then create a new instance.
Expand Down Expand Up @@ -313,7 +315,7 @@ private PooledObject borrowObjectNoTest() throws InterruptedException {
return pooledConn;
}
}
} while (nanos > 0);
}
} finally {
lock.unlock();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.impl.api;

import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.identityconnectors.common.Version;
import org.identityconnectors.common.logging.Log;
Expand Down Expand Up @@ -86,6 +94,51 @@ public void testCheckVersion() throws Exception {
}
}

/**
* A bundle entry such as {@code lib/../../x.jar} must not be written
* outside of the bundle's temporary directory (zip slip).
*/
@Test
public void testRejectsBundleEntryEscapingTempDirectory() throws Exception {
// bundles are expanded into java.io.tmpdir/bundle-<random>/, so the
// entry below points two levels up, straight into java.io.tmpdir
String escapedName = "escaped-" + UUID.randomUUID() + ".jar";
File escaped = new File(System.getProperty("java.io.tmpdir"), escapedName);

File bundle = File.createTempFile("evil-bundle", ".jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().putValue("ConnectorBundle-FrameworkVersion", "1.0");
manifest.getMainAttributes().putValue("ConnectorBundle-Name", "evil");
manifest.getMainAttributes().putValue("ConnectorBundle-Version", "1.0");
JarOutputStream out = new JarOutputStream(new FileOutputStream(bundle), manifest);
try {
// a regular entry first, so that lib/ exists when the escaping
// entry is expanded and lib/../.. resolves
out.putNextEntry(new JarEntry("lib/ok.jar"));
out.write(new byte[] { 0 });
out.closeEntry();
out.putNextEntry(new JarEntry("lib/../../" + escapedName));
out.write(new byte[] { 0 });
out.closeEntry();
} finally {
out.close();
}
try {
ConnectorInfoManagerFactory.getInstance().getLocalManager(bundle.toURI().toURL());
Assert.fail("Expected the bundle to be refused");
} catch (ConfigurationException expected) {
assertFalse(escaped.exists(), "bundle entry written outside of its temp directory: "
+ escaped);
} finally {
// nothing to evict: a bundle that fails to load is never cached,
// and clearing the local cache here would leave pooled connector
// instances of the other tests behind with a stale class loader
escaped.delete();
bundle.delete();
}
}

/**
* To be overridden by subclasses to get different ConnectorInfoManagers
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.forgerock.openicf.framework.remote;

import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -56,11 +55,9 @@
import org.forgerock.openicf.framework.async.impl.ValidateAsyncApiOpImpl;
import org.forgerock.openicf.framework.remote.rpc.OperationMessageListener;
import org.forgerock.openicf.framework.remote.rpc.WebSocketConnectionHolder;
import org.forgerock.openicf.framework.remote.security.ECIESEncryptor;
import org.identityconnectors.common.Assertions;
import org.identityconnectors.common.l10n.CurrentLocale;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.Encryptor;
import org.identityconnectors.framework.api.ConfigurationProperty;
import org.identityconnectors.framework.api.ConfigurationPropertyChangeListener;
import org.identityconnectors.framework.api.ConnectorFacade;
Expand Down Expand Up @@ -491,17 +488,6 @@ public void processCancelOpRequest(final WebSocketConnectionHolder socket, long
messageId);
}

protected Encryptor initialiseEncryptor() {
HandshakeMessage message = null;
// Create Encryptor
if (!message.getPublicKey().isEmpty()) {
PublicKey publicKey =
SecurityUtil.createPublicKey(message.getPublicKey().toByteArray());
Encryptor encryptor = new ECIESEncryptor(keyPair, publicKey);
}
return null;
}

protected String loggerName() {
return isClient() ? "Client" : "Server";
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,19 @@
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* Portions Copyrighted 2026 3A Systems, LLC
*/

package org.forgerock.openicf.framework.remote;

import org.forgerock.openicf.framework.remote.security.ECIESEncryptor;
import org.identityconnectors.common.Pair;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.security.KeyPair;
import java.security.SecureRandom;

public class SecurityUtilTest {

@Test
public void testECIESEncryptor() throws Exception {
KeyPair client = SecurityUtil.generateKeyPair();
KeyPair server = SecurityUtil.generateKeyPair();

ECIESEncryptor clientEncryptor = new ECIESEncryptor(client, server.getPublic());
ECIESEncryptor serverEncryptor = new ECIESEncryptor(server, client.getPublic());

byte[] expected = "password".getBytes();
byte[] secure = clientEncryptor.encrypt(expected);
Assert.assertEquals(serverEncryptor.decrypt(secure), expected);
}

@Test
public void testCheckMutualVerification() throws Exception {
SecureRandom random = new SecureRandom();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,32 @@ public static void extractResourceToFile(final Class<?> clazz, final String path
}
}

/**
* Resolves an archive entry name against a directory, refusing names that
* would land outside of it, such as {@code ../etc/passwd} (zip slip).
*
* @param dir
* The directory the entry is extracted into.
* @param entryName
* The entry name as recorded in the archive.
* @return The canonical file the entry maps to: {@code dir} itself or a
* file below it.
* @throws IOException
* If the entry would escape {@code dir}.
*/
public static File resolveEntry(final File dir, final String entryName) throws IOException {
final File root = dir.getCanonicalFile();
final File file = new File(root, entryName).getCanonicalFile();
final String rootPath = root.getPath();
final String prefix = rootPath.endsWith(File.separator) ? rootPath : rootPath + File.separator;
// the trailing separator lets the directory itself pass ("dir/")
// and keeps a sibling such as "dir2" out
if (!(file.getPath() + File.separator).startsWith(prefix)) {
throw new IOException("Archive entry " + entryName + " is outside of " + dir);
}
return file;
}

/**
* Unjars the given file to the given directory. Does not close the JarFile
* when finished.
Expand All @@ -630,7 +656,7 @@ public static void unjar(final JarFile jarFile, final File toDir) throws IOExcep
final Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
final File outFile = new File(toDir, entry.getName());
final File outFile = resolveEntry(toDir, entry.getName());
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
FileOutputStream fos = null;
try {
fos = new FileOutputStream(outFile);
Expand Down
Loading
Loading