diff --git a/jooby/src/main/java/org/jooby/Cookie.java b/jooby/src/main/java/org/jooby/Cookie.java index 2cf5cbd4..d29179d2 100644 --- a/jooby/src/main/java/org/jooby/Cookie.java +++ b/jooby/src/main/java/org/jooby/Cookie.java @@ -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); diff --git a/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java b/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java index 3b8b23e7..3635a470 100644 --- a/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java +++ b/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java @@ -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"); } } diff --git a/jooby/src/main/java/org/jooby/handlers/SSIHandler.java b/jooby/src/main/java/org/jooby/handlers/SSIHandler.java index f4f6e60b..d5d81ca2 100644 --- a/jooby/src/main/java/org/jooby/handlers/SSIHandler.java +++ b/jooby/src/main/java/org/jooby/handlers/SSIHandler.java @@ -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()); } } } diff --git a/jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java b/jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java new file mode 100644 index 00000000..52d923ee --- /dev/null +++ b/jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java @@ -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")); + } + } +} diff --git a/jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java b/jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java new file mode 100644 index 00000000..cd773ca7 --- /dev/null +++ b/jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java @@ -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()); + } +}