Skip to content
Merged
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 @@ -86,29 +86,29 @@ public DetectionResult scan(String prompt) {
}

// Tokenise: split on whitespace + common punctuation, lowercase everything
String[] tokens = tokenise(prompt);
if (tokens.length == 0) {
String[] words = tokenise(prompt);
if (words.length == 0) {
return DetectionResult.clean();
}

// Collect all matched keywords (position → keyword) for phrase-window check
Map<Integer, String> hits = new HashMap<>();
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i];
if (token.length() < minWordLength) {
for (int i = 0; i < words.length; i++) {
String word = words[i];
if (word.length() < minWordLength) {
continue;
}

Fingerprint fp = Fingerprint.of(token);
Fingerprint fp = Fingerprint.of(word);
List<String> candidates = index.get(fp);
if (candidates == null) {
continue;
}

for (String keyword : candidates) {
if (isTypoglycemiaMatch(token, keyword)) {
if (isTypoglycemiaMatch(word, keyword)) {
hits.put(i, keyword);
logger.debug("Typoglycemia hit: token='{}' matches keyword='{}' at pos={}", token, keyword, i);
logger.debug("Typoglycemia hit: word='{}' matches keyword='{}' at pos={}", word, keyword, i);
break;
}
}
Expand All @@ -120,7 +120,7 @@ public DetectionResult scan(String prompt) {

// A single-token hit on a high-value keyword is sufficient to flag
String matchedKeyword = hits.values().iterator().next();
double score = computeScore(hits, tokens.length);
double score = computeScore(hits, words.length);

return DetectionResult.injection(
score,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/

package org.forgerock.openig.jwt;
Expand Down Expand Up @@ -199,8 +200,8 @@ public static class Heaplet extends GenericHeaplet {

private static final Logger logger = LoggerFactory.getLogger(Heaplet.class);

/** RSA needs at least a 512 key length.*/
private static final int KEY_SIZE = 1024;
/** RSA key size (in bits) for the temporary KeyPair generated when no KeyStore is configured. */
private static final int KEY_SIZE = 2048;

@Override
public Object create() throws HeapException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,30 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.forgerock.openig.jwt;

import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.MockitoAnnotations.initMocks;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;

import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.http.session.Session;
import org.forgerock.json.jose.jws.handlers.HmacSigningHandler;
import org.forgerock.openig.heap.HeapUtilsTest;
import org.forgerock.openig.heap.Name;
import org.mockito.Mock;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -61,4 +70,19 @@ public void shouldNotSaveSession() throws Exception {
manager.save(session, null);
verifyNoMoreInteractions(session);
}

@Test
public void shouldGenerateAtLeast2048BitKeyPairWhenNoKeystoreIsConfigured() throws Exception {
JwtSessionManager created = (JwtSessionManager) new JwtSessionManager.Heaplet()
.create(Name.of("this"), json(object()), HeapUtilsTest.buildDefaultHeap());

RSAPublicKey publicKey = (RSAPublicKey) keyPairOf(created).getPublic();
assertThat(publicKey.getModulus().bitLength()).isGreaterThanOrEqualTo(2048);
}

private static KeyPair keyPairOf(JwtSessionManager manager) throws Exception {
Field field = JwtSessionManager.class.getDeclaredField("keyPair");
field.setAccessible(true);
return (KeyPair) field.get(manager);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
* Copyright 2018 3A Systems, LLC
* Copyright 2018-2026 3A Systems, LLC.
*/

package org.forgerock.openig.openam;
Expand Down Expand Up @@ -152,7 +152,7 @@ public Promise<Response, NeverThrowsException> filter(final Context context,
final String issued_token=cache.getIfPresent(resolvedIdToken);
if (issued_token!=null) {
if (logger.isTraceEnabled()) {
logger.trace("get ftrom cache {}", issued_token);
logger.trace("issued token found in cache (length={})", issued_token.length());
}
return next.handle(new StsContext(context, issued_token), request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/

package org.forgerock.openig.web;
Expand Down Expand Up @@ -70,16 +71,8 @@ class UiAdminHttpApplication extends AdminHttpApplication {

// Unpack it in the OpenIG temp directory (create sub-directory)
File unpack = new File(environment.getTempDirectory(), "openig-ui");
try (JarInputStream jar = new JarInputStream(new BufferedInputStream(url.openStream()))) {
JarEntry entry = jar.getNextJarEntry();
while (entry != null) {
if (!entry.isDirectory()) {
unpackFileEntry(jar, entry, new File(unpack, entry.getName()));
}
// Close and move to the next entry
jar.closeEntry();
entry = jar.getNextJarEntry();
}
try (InputStream in = url.openStream()) {
unpackJar(in, unpack);
}

// Create a FileResourceSet around that directory
Expand All @@ -94,6 +87,34 @@ class UiAdminHttpApplication extends AdminHttpApplication {
getOpenIGRouter().addRoute(requestUriMatcher(STARTS_WITH, "studio"), handler);
}

/**
* Unpacks all file entries of the given jar stream into the {@code unpack} directory.
*
* @param in the jar content
* @param unpack the directory to unpack file entries into
* @throws IOException when unpack fails
*/
static void unpackJar(final InputStream in, final File unpack) throws IOException {
String unpackPath = unpack.getCanonicalPath() + File.separator;
try (JarInputStream jar = new JarInputStream(new BufferedInputStream(in))) {
JarEntry entry = jar.getNextJarEntry();
while (entry != null) {
if (!entry.isDirectory()) {
File destination = new File(unpack, entry.getName());
// Reject entries that would escape the unpack directory (zip slip)
if (!destination.getCanonicalPath().startsWith(unpackPath)) {
throw new IOException("Jar entry '" + entry.getName()
+ "' is outside of the unpack directory " + unpack);
}
unpackFileEntry(jar, entry, destination);
}
// Close and move to the next entry
jar.closeEntry();
entry = jar.getNextJarEntry();
}
}
}

private static void unpackFileEntry(final JarInputStream jar, final JarEntry entry, final File destination)
throws IOException {
// Prepare parent directories if they do not exists yet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,28 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/

package org.forgerock.openig.web;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.forgerock.openig.http.RunMode.EVALUATION;
import static org.forgerock.openig.web.OpenIGInitializerTest.getRelative;
import static org.forgerock.services.context.ClientContext.newInternalClientContext;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import org.forgerock.http.Handler;
import org.forgerock.http.header.ContentTypeHeader;
Expand Down Expand Up @@ -52,6 +62,48 @@ public void shouldServeTheUi() throws Exception {
assertThat(response.getHeaders().getFirst(ContentTypeHeader.class)).isEqualTo("text/html");
}

@Test
public void shouldUnpackRegularJarEntryIntoUnpackDirectory() throws Exception {
File root = Files.createTempDirectory("openig-ui").toFile();
File unpack = new File(root, "openig-ui");

UiAdminHttpApplication.unpackJar(jarWithEntry("index.html", "<html/>"), unpack);

assertThat(new File(unpack, "index.html")).hasContent("<html/>");
}

@Test
public void shouldRejectJarEntryEscapingUnpackDirectory() throws Exception {
File root = Files.createTempDirectory("openig-ui").toFile();
File unpack = new File(root, "openig-ui");

// A regular entry first, so that the unpack directory exists when the escaping entry is processed
ByteArrayInputStream jar = jarWithEntries("index.html", "<html/>",
"../evil.txt", "boom");

assertThatThrownBy(() -> UiAdminHttpApplication.unpackJar(jar, unpack))
.isInstanceOf(IOException.class)
.hasMessageContaining("../evil.txt");
assertThat(new File(root, "evil.txt")).doesNotExist();
}

private static ByteArrayInputStream jarWithEntry(String name, String content) throws IOException {
return jarWithEntries(name, content);
}

/** Builds an in-memory jar from {@code name, content} pairs, in the given order. */
private static ByteArrayInputStream jarWithEntries(String... nameContentPairs) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JarOutputStream jar = new JarOutputStream(bytes)) {
for (int i = 0; i < nameContentPairs.length; i += 2) {
jar.putNextEntry(new JarEntry(nameContentPairs[i]));
jar.write(nameContentPairs[i + 1].getBytes(StandardCharsets.UTF_8));
jar.closeEntry();
}
}
return new ByteArrayInputStream(bytes.toByteArray());
}

private static UriRouterContext newUriRouterContext(Context parent) {
return new UriRouterContext(parent,
"",
Expand Down
Loading