Skip to content

Commit 7bf1b15

Browse files
authored
Merge pull request #4023 from jooby-project/4022
fix: pac4j: Add defense-in-depth escaping and startup warning to DevLoginForm
2 parents 7e01589 + fb8c9cf commit 7bf1b15

4 files changed

Lines changed: 106 additions & 7 deletions

File tree

‎modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/DevLoginForm.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.jooby.Context;
1212
import io.jooby.MediaType;
1313
import io.jooby.Route;
14+
import io.jooby.XSS;
1415
import io.jooby.pac4j.Pac4jContext;
1516

1617
public class DevLoginForm implements Route.Handler {
@@ -68,8 +69,8 @@ public DevLoginForm(Config pac4j, String callbackPath) {
6869

6970
@Override
7071
public Object apply(Context ctx) throws Exception {
71-
String error = ctx.query("error").value("");
72-
String username = ctx.query("username").value("");
72+
String error = XSS.html(ctx.query("error").value(""));
73+
String username = XSS.html(ctx.query("username").value(""));
7374

7475
ctx.setAttribute("username", username);
7576
ctx.setAttribute("error", error);

‎modules/jooby-pac4j/src/main/java/io/jooby/pac4j/Pac4jModule.java‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void add(String authorizer, Object client) {
8989

9090
private final Pac4jOptions options;
9191

92-
private Map<String, ProtectedPath> clientMap;
92+
private @Nullable Map<String, ProtectedPath> clientMap;
9393

9494
/** Creates a new pac4j module. */
9595
public Pac4jModule() {
@@ -353,9 +353,8 @@ public void install(Jooby app) throws Exception {
353353
Map<String, List<ClientReference>> allClients = new LinkedHashMap<>();
354354

355355
/* Should add simple login form? */
356-
var devLogin = false;
357-
if (clientMap.isEmpty()) {
358-
devLogin = true;
356+
var devLogin = app.getEnvironment().isActive("dev", "test");
357+
if (clientMap.isEmpty() && devLogin) {
359358
client(
360359
conf ->
361360
new FormClient(
@@ -441,6 +440,15 @@ public void install(Jooby app) throws Exception {
441440
ofNullable(options.getProfileManagerFactory()).orElse(ProfileManagerFactory.DEFAULT));
442441

443442
if (devLogin) {
443+
var devLoginFormWarning =
444+
!conf.hasPath("pac4j.devLoginFormWarning")
445+
|| conf.getBoolean("pac4j.devLoginFormWarning");
446+
if (devLoginFormWarning) {
447+
app.getLog()
448+
.warn(
449+
"Pac4j module is using the fallback DevLoginForm. This component is for development"
450+
+ " purposes only and should NOT be used in production.");
451+
}
444452
app.get("/login", new DevLoginForm(options, contextPath + options.getCallbackPath()));
445453
}
446454

‎modules/jooby-pac4j/src/test/java/io/jooby/internal/pac4j/DevLoginFormTest.java‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
*/
66
package io.jooby.internal.pac4j;
77

8+
import static org.junit.jupiter.api.Assertions.assertFalse;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
810
import static org.mockito.Mockito.*;
911

1012
import org.junit.jupiter.api.BeforeEach;
1113
import org.junit.jupiter.api.Test;
14+
import org.mockito.ArgumentCaptor;
1215
import org.pac4j.core.client.Clients;
1316
import org.pac4j.core.config.Config;
17+
import org.pac4j.core.context.WebContext;
1418
import org.pac4j.core.http.url.UrlResolver;
1519

1620
import io.jooby.Context;
@@ -98,4 +102,52 @@ void testApplyWithMissingQueryData() throws Exception {
98102
&& html.contains("action=\"/callback?client_name=FormClient\"")
99103
&& html.contains("value=\"\"")));
100104
}
105+
106+
@Test
107+
public void shouldEscapeXssPayloads() throws Exception {
108+
Config config = mock(Config.class);
109+
Clients clients = mock(Clients.class);
110+
UrlResolver urlResolver = mock(UrlResolver.class);
111+
112+
when(config.getClients()).thenReturn(clients);
113+
when(clients.getUrlResolver()).thenReturn(urlResolver);
114+
when(urlResolver.compute(anyString(), any(WebContext.class))).thenReturn("/callback");
115+
116+
Context ctx = mock(Context.class);
117+
Value errorNode = mock(Value.class);
118+
Value userNode = mock(Value.class);
119+
120+
String xssError = "<script>alert('error')</script>";
121+
String xssUser = "\"><script>alert('user')</script>";
122+
123+
when(ctx.query("error")).thenReturn(errorNode);
124+
when(errorNode.value("")).thenReturn(xssError);
125+
126+
when(ctx.query("username")).thenReturn(userNode);
127+
when(userNode.value("")).thenReturn(xssUser);
128+
129+
// Mock response chain
130+
when(ctx.setResponseType(MediaType.html)).thenReturn(ctx);
131+
132+
// Capture the HTML argument passed to ctx.send() and return the ctx mock
133+
ArgumentCaptor<String> htmlCaptor = ArgumentCaptor.forClass(String.class);
134+
when(ctx.send(htmlCaptor.capture())).thenReturn(ctx);
135+
136+
DevLoginForm form = new DevLoginForm(config, "/callback");
137+
form.apply(ctx);
138+
139+
// Retrieve the captured HTML
140+
String html = htmlCaptor.getValue();
141+
142+
// Assert inputs were sanitized
143+
assertFalse(html.contains(xssError), "Error payload was rendered unescaped!");
144+
assertFalse(html.contains(xssUser), "Username payload was rendered unescaped!");
145+
146+
// Verify HTML entity encoding occurred (checking < and >)
147+
assertTrue(html.contains("&lt;script&gt;"), "Expected escaped <script> tag");
148+
149+
// Verify quotes were escaped in the value attribute injection
150+
assertTrue(
151+
html.contains("&quot;&gt;") || html.contains("&#34;&gt;"), "Expected escaped double quote");
152+
}
101153
}

‎modules/jooby-pac4j/src/test/java/io/jooby/pac4j/Pac4jModuleTest.java‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import org.pac4j.core.client.Clients;
2020
import org.pac4j.core.config.Config;
2121
import org.pac4j.http.client.direct.DirectBasicAuthClient;
22+
import org.slf4j.Logger;
2223

24+
import io.jooby.Environment;
2325
import io.jooby.Jooby;
2426
import io.jooby.ServiceRegistry;
2527
import io.jooby.StatusCode;
@@ -29,16 +31,22 @@
2931
public class Pac4jModuleTest {
3032

3133
private Jooby app;
34+
private Logger log;
35+
private Environment env;
3236
private ServiceRegistry registry;
3337
private com.typesafe.config.Config config;
3438

3539
@BeforeEach
3640
void setUp() {
3741
app = mock(Jooby.class);
42+
log = mock(Logger.class);
43+
env = mock(Environment.class);
3844
registry = mock(ServiceRegistry.class);
3945
config = mock(com.typesafe.config.Config.class);
4046

4147
when(app.getServices()).thenReturn(registry);
48+
when(app.getLog()).thenReturn(log);
49+
when(app.getEnvironment()).thenReturn(env);
4250
when(app.getConfig()).thenReturn(config);
4351
when(app.getContextPath()).thenReturn("/");
4452
}
@@ -76,8 +84,38 @@ void testClientDSLVariants() {
7684
}
7785

7886
@Test
79-
void testInstallDefaultLogin() throws Exception {
87+
void testInstallDevLoginWithWarning() throws Exception {
8088
Pac4jModule module = new Pac4jModule();
89+
when(env.isActive("dev", "test")).thenReturn(true);
90+
91+
module.install(app);
92+
93+
verify(app).get(eq("/login"), any());
94+
verify(app).get(eq("/callback"), any());
95+
verify(app).post(eq("/callback"), any());
96+
}
97+
98+
@Test
99+
void testInstallDevLoginWithWarningOff() throws Exception {
100+
Pac4jModule module = new Pac4jModule();
101+
when(config.hasPath("pac4j.devLoginFormWarning")).thenReturn(true);
102+
when(config.getBoolean("pac4j.devLoginFormWarning")).thenReturn(false);
103+
when(env.isActive("dev", "test")).thenReturn(true);
104+
105+
module.install(app);
106+
107+
verify(app).get(eq("/login"), any());
108+
verify(app).get(eq("/callback"), any());
109+
verify(app).post(eq("/callback"), any());
110+
}
111+
112+
@Test
113+
void testInstallDevLoginWithWarningOn() throws Exception {
114+
Pac4jModule module = new Pac4jModule();
115+
when(config.hasPath("pac4j.devLoginFormWarning")).thenReturn(true);
116+
when(config.getBoolean("pac4j.devLoginFormWarning")).thenReturn(true);
117+
when(env.isActive("dev", "test")).thenReturn(true);
118+
81119
module.install(app);
82120

83121
verify(app).get(eq("/login"), any());

0 commit comments

Comments
 (0)