feature: Add extra feedback once someone solves JWT refresh lesson differently

One can solve this lesson by using `alg:none` instead of using the refresh token flow. Instead of adding a check to force using the refresh token we opt for giving the user extra feedback.
This commit is contained in:
Nanne Baars 2023-02-16 17:32:13 +00:00
parent 73b8c431fc
commit ecfc321f14
6 changed files with 49 additions and 56 deletions

View File

@ -22,7 +22,12 @@
package org.owasp.webgoat.lessons.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.impl.TextCodec;
import java.sql.ResultSet;
import java.sql.SQLException;
@ -31,34 +36,12 @@ import org.owasp.webgoat.container.LessonDataSource;
import org.owasp.webgoat.container.assignments.AssignmentEndpoint;
import org.owasp.webgoat.container.assignments.AssignmentHints;
import org.owasp.webgoat.container.assignments.AttackResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
*
*
* <pre>
* {
* "typ": "JWT",
* "kid": "webgoat_key",
* "alg": "HS256"
* }
* {
* "iss": "WebGoat Token Builder",
* "iat": 1524210904,
* "exp": 1618905304,
* "aud": "webgoat.org",
* "sub": "jerry@webgoat.com",
* "username": "Jerry",
* "Email": "jerry@webgoat.com",
* "Role": [
* "Cat"
* ]
* }
* </pre>
*
* @author nbaars
* @since 4/23/17.
*/
@RestController
@AssignmentHints({
"jwt-final-hint1",

View File

@ -49,10 +49,6 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author nbaars
* @since 4/23/17.
*/
@RestController
@AssignmentHints({
"jwt-refresh-hint1",
@ -85,9 +81,7 @@ public class JWTRefreshEndpoint extends AssignmentEndpoint {
}
private Map<String, Object> createNewTokens(String user) {
Map<String, Object> claims = new HashMap<>();
claims.put("admin", "false");
claims.put("user", user);
Map<String, Object> claims = Map.of("admin", "false", "user", user);
String token =
Jwts.builder()
.setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10)))
@ -114,6 +108,9 @@ public class JWTRefreshEndpoint extends AssignmentEndpoint {
Claims claims = (Claims) jwt.getBody();
String user = (String) claims.get("user");
if ("Tom".equals(user)) {
if ("none".equals(jwt.getHeader().get("alg"))) {
return ok(success(this).feedback("jwt-refresh-alg-none").build());
}
return ok(success(this).build());
}
return ok(failed(this).feedback("jwt-refresh-not-tom").feedbackArgs(user).build());

View File

@ -42,10 +42,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author nbaars
* @since 4/23/17.
*/
@RestController
@AssignmentHints({"jwt-secret-hint1", "jwt-secret-hint2", "jwt-secret-hint3"})
public class JWTSecretKeyEndpoint extends AssignmentEndpoint {

View File

@ -58,10 +58,6 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
/**
* @author nbaars
* @since 4/23/17.
*/
@RestController
@AssignmentHints({
"jwt-change-token-hint1",

View File

@ -21,6 +21,7 @@ jwt-refresh-hint2=The token from the access log is no longer valid, can you find
jwt-refresh-hint3=The endpoint for refreshing a token is 'JWT/refresh/newToken'
jwt-refresh-hint4=Use the found access token in the Authorization: Bearer header and use your own refresh token
jwt-refresh-not-tom=User is not Tom but {0}, please try again
jwt-refresh-alg-none=Nicely found! You solved the assignment with 'alg: none' can you also solve it by using the refresh token?
jwt-final-jerry-account=Yikes, you are removing Jerry's account, try to delete the account of Tom
jwt-final-not-tom=Username is not Tom try to pass a token for Tom
@ -30,4 +31,4 @@ jwt-final-hint2=The 'kid' (key ID) header parameter is a hint indicating which k
jwt-final-hint3=The key can be located on the filesystem in memory or even reside in the database
jwt-final-hint4=The key is stored in the database and loaded while verifying a token
jwt-final-hint5=Using a SQL injection you might be able to manipulate the key to something you know and create a new token.
jwt-final-hint6=Use: hacked' UNION select 'deletingTom' from INFORMATION_SCHEMA.SYSTEM_USERS -- as the kid in the header and change the contents of the token to Tom and hit the endpoint with the new token
jwt-final-hint6=Use: hacked' UNION select 'deletingTom' from INFORMATION_SCHEMA.SYSTEM_USERS -- as the kid in the header and change the contents of the token to Tom and hit the endpoint with the new token

View File

@ -29,6 +29,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import java.util.HashMap;
import java.util.Map;
import org.hamcrest.CoreMatchers;
@ -43,14 +44,14 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
public class JWTRefreshEndpointTest extends LessonTest {
@BeforeEach
public void setup() {
void setup() {
when(webSession.getCurrentLesson()).thenReturn(new JWT());
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
when(webSession.getUserName()).thenReturn("unit-test");
}
@Test
public void solveAssignment() throws Exception {
void solveAssignment() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
// First login to obtain tokens for Jerry
@ -96,7 +97,26 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void checkoutWithTomsTokenFromAccessLogShouldFail() throws Exception {
void solutionWithAlgNone() throws Exception {
String tokenWithNoneAlgorithm =
Jwts.builder()
.setHeaderParam("alg", "none")
.addClaims(Map.of("admin", "true", "user", "Tom"))
.compact();
// Now checkout with the new token from Tom
mockMvc
.perform(
MockMvcRequestBuilders.post("/JWT/refresh/checkout")
.header("Authorization", "Bearer " + tokenWithNoneAlgorithm))
.andExpect(status().isOk())
.andExpect(jsonPath("$.lessonCompleted", is(true)))
.andExpect(
jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-refresh-alg-none"))));
}
@Test
void checkoutWithTomsTokenFromAccessLogShouldFail() throws Exception {
String accessTokenTom =
"eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q";
mockMvc
@ -108,7 +128,7 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void checkoutWitRandomTokenShouldFail() throws Exception {
void checkoutWitRandomTokenShouldFail() throws Exception {
String accessTokenTom =
"eyJhbGciOiJIUzUxMiJ9.eyJpLXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q";
mockMvc
@ -121,7 +141,7 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void flowForJerryAlwaysWorks() throws Exception {
void flowForJerryAlwaysWorks() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
var loginJson = Map.of("user", "Jerry", "password", PASSWORD);
@ -146,7 +166,7 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void loginShouldNotWorkForJerryWithWrongPassword() throws Exception {
void loginShouldNotWorkForJerryWithWrongPassword() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
var loginJson = Map.of("user", "Jerry", "password", PASSWORD + "wrong");
@ -159,7 +179,7 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void loginShouldNotWorkForTom() throws Exception {
void loginShouldNotWorkForTom() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
var loginJson = Map.of("user", "Tom", "password", PASSWORD);
@ -172,7 +192,7 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void newTokenShouldWorkForJerry() throws Exception {
void newTokenShouldWorkForJerry() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> loginJson = new HashMap<>();
loginJson.put("user", "Jerry");
@ -201,7 +221,7 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void unknownRefreshTokenShouldGiveUnauthorized() throws Exception {
void unknownRefreshTokenShouldGiveUnauthorized() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> loginJson = new HashMap<>();
loginJson.put("user", "Jerry");
@ -229,21 +249,21 @@ public class JWTRefreshEndpointTest extends LessonTest {
}
@Test
public void noTokenWhileCheckoutShouldReturn401() throws Exception {
void noTokenWhileCheckoutShouldReturn401() throws Exception {
mockMvc
.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout"))
.andExpect(status().isUnauthorized());
}
@Test
public void noTokenWhileRequestingNewTokenShouldReturn401() throws Exception {
void noTokenWhileRequestingNewTokenShouldReturn401() throws Exception {
mockMvc
.perform(MockMvcRequestBuilders.post("/JWT/refresh/newToken"))
.andExpect(status().isUnauthorized());
}
@Test
public void noTokenWhileLoginShouldReturn401() throws Exception {
void noTokenWhileLoginShouldReturn401() throws Exception {
mockMvc
.perform(MockMvcRequestBuilders.post("/JWT/refresh/login"))
.andExpect(status().isUnauthorized());