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
4 changes: 2 additions & 2 deletions jooby/src/main/java/org/jooby/Cookie.java
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,8 @@ public static String sign(final String value, final String secret) {

try {
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(secret.getBytes(), HMAC_SHA256));
byte[] bytes = mac.doFinal(value.getBytes());
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
byte[] bytes = mac.doFinal(value.getBytes(StandardCharsets.UTF_8));
return EQ.matcher(BaseEncoding.base64().encode(bytes)).replaceAll("") + SEP + value;
} catch (Exception ex) {
throw new IllegalArgumentException("Can't sign value", ex);
Expand Down
2 changes: 1 addition & 1 deletion jooby/src/main/java/org/jooby/handlers/CsrfHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public void handle(final Request req, final Response rsp, final Route.Chain chai
String candidate = req.header(name).toOptional()
.orElseGet(() -> req.param(name).toOptional().orElse(null));
if (!token.equals(candidate)) {
throw new Err(Status.FORBIDDEN, "Invalid Csrf token: " + candidate);
throw new Err(Status.FORBIDDEN, "Invalid CSRF token");
}
}

Expand Down
12 changes: 8 additions & 4 deletions jooby/src/main/java/org/jooby/handlers/SSIHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,18 @@ private String process(final Env env, final String src) {

private String file(final String key) {
String file = Route.normalize(key.trim());
return text(getClass().getResourceAsStream(file));
InputStream stream = getClass().getResourceAsStream(file);
if (stream == null) {
throw new NoSuchElementException("Resource not found: " + file);
}
return text(stream);
}

private String text(final InputStream stream) {
try (InputStream in = stream) {
return CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8));
} catch (IOException | NullPointerException x) {
throw new NoSuchElementException();
return CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8));
} catch (IOException x) {
throw new NoSuchElementException(x.getMessage());
}
}
}
147 changes: 147 additions & 0 deletions jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright 2026 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.jooby.handlers;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;

import org.jooby.Err;
import org.jooby.Mutant;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Route;
import org.jooby.Session;
import org.junit.Test;

public class CsrfHandlerTest {

@Test
public void invalidTokenDoesNotEchoCandidate() throws Throwable {
Request req = mock(Request.class);
Response rsp = mock(Response.class);
Route.Chain chain = mock(Route.Chain.class);
Session session = mock(Session.class);
Mutant tokenMutant = mock(Mutant.class);
Mutant headerMutant = mock(Mutant.class);
Mutant paramMutant = mock(Mutant.class);

when(req.session()).thenReturn(session);
when(req.method()).thenReturn("POST");
when(session.get("csrf")).thenReturn(tokenMutant);
when(tokenMutant.toOptional()).thenReturn(Optional.of("real-token"));
when(req.header("csrf")).thenReturn(headerMutant);
when(headerMutant.toOptional()).thenReturn(Optional.empty());
when(req.param("csrf")).thenReturn(paramMutant);
when(paramMutant.toOptional()).thenReturn(Optional.of("attacker-token"));
when(req.set(anyString(), any())).thenReturn(req);

CsrfHandler handler = new CsrfHandler();
try {
handler.handle(req, rsp, chain);
fail("Expected Err to be thrown");
} catch (Err err) {
assertEquals(403, err.statusCode());
assertTrue("Error message should contain 'Invalid CSRF token'",
err.getMessage().contains("Invalid CSRF token"));
assertTrue("Error message must not contain the candidate token",
!err.getMessage().contains("attacker-token"));
}
}

@Test
public void validTokenPassesThrough() throws Throwable {
Request req = mock(Request.class);
Response rsp = mock(Response.class);
Route.Chain chain = mock(Route.Chain.class);
Session session = mock(Session.class);
Mutant tokenMutant = mock(Mutant.class);
Mutant headerMutant = mock(Mutant.class);

String token = "valid-token";

when(req.session()).thenReturn(session);
when(req.method()).thenReturn("POST");
when(session.get("csrf")).thenReturn(tokenMutant);
when(tokenMutant.toOptional()).thenReturn(Optional.of(token));
when(req.header("csrf")).thenReturn(headerMutant);
when(headerMutant.toOptional()).thenReturn(Optional.of(token));
when(req.set(anyString(), any())).thenReturn(req);

CsrfHandler handler = new CsrfHandler();
handler.handle(req, rsp, chain);

verify(chain).next(req, rsp);
}

@Test
public void getRequestSkipsTokenVerification() throws Throwable {
Request req = mock(Request.class);
Response rsp = mock(Response.class);
Route.Chain chain = mock(Route.Chain.class);
Session session = mock(Session.class);
Mutant tokenMutant = mock(Mutant.class);

when(req.session()).thenReturn(session);
when(req.method()).thenReturn("GET");
when(session.get("csrf")).thenReturn(tokenMutant);
when(tokenMutant.toOptional()).thenReturn(Optional.of("some-token"));
when(req.set(anyString(), any())).thenReturn(req);

CsrfHandler handler = new CsrfHandler();
handler.handle(req, rsp, chain);

verify(chain).next(req, rsp);
}

@Test
public void missingTokenThrowsForbidden() throws Throwable {
Request req = mock(Request.class);
Response rsp = mock(Response.class);
Route.Chain chain = mock(Route.Chain.class);
Session session = mock(Session.class);
Mutant tokenMutant = mock(Mutant.class);
Mutant headerMutant = mock(Mutant.class);
Mutant paramMutant = mock(Mutant.class);

when(req.session()).thenReturn(session);
when(req.method()).thenReturn("POST");
when(session.get("csrf")).thenReturn(tokenMutant);
when(tokenMutant.toOptional()).thenReturn(Optional.of("real-token"));
when(req.header("csrf")).thenReturn(headerMutant);
when(headerMutant.toOptional()).thenReturn(Optional.empty());
when(req.param("csrf")).thenReturn(paramMutant);
when(paramMutant.toOptional()).thenReturn(Optional.empty());
when(req.set(anyString(), any())).thenReturn(req);

CsrfHandler handler = new CsrfHandler();
try {
handler.handle(req, rsp, chain);
fail("Expected Err to be thrown");
} catch (Err err) {
assertEquals(403, err.statusCode());
assertTrue("Error message should contain 'Invalid CSRF token'",
err.getMessage().contains("Invalid CSRF token"));
}
}
}
61 changes: 61 additions & 0 deletions jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2026 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.jooby.handlers;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.NoSuchElementException;

import org.junit.Test;

public class SSIHandlerTest {

@Test
public void missingResourceIncludesPathInMessage() throws Exception {
SSIHandler handler = new SSIHandler();

Method fileMethod = SSIHandler.class.getDeclaredMethod("file", String.class);
fileMethod.setAccessible(true);

try {
fileMethod.invoke(handler, "/nonexistent/resource.html");
fail("Expected NoSuchElementException");
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertTrue("Expected NoSuchElementException but got " + cause.getClass(),
cause instanceof NoSuchElementException);
String message = cause.getMessage();
assertTrue("Exception message should contain the resource path, got: " + message,
message.contains("/nonexistent/resource.html"));
}
}

@Test
public void existingResourceReturnsContent() throws Exception {
SSIHandler handler = new SSIHandler();

Method fileMethod = SSIHandler.class.getDeclaredMethod("file", String.class);
fileMethod.setAccessible(true);

// Use a resource that definitely exists on the classpath
String result = (String) fileMethod.invoke(handler, "/org/jooby/mime.properties");
assertTrue("Should return non-empty content", result != null && !result.isEmpty());
}
}
Loading