Password reset link test condition more strict and move all WebWolf links to /WebWolf (#1645)

* better check on host and port for password reset and make context roots more flexible

* spotless applied

* removed hardcoded /WebGoat from js

* removed hardcoded /WebGoat from js

* fix spotless

* fix scoreboard

* upgrade WebWolf bootstrap version and icons and templates - part 1

* fixed more bootstrap 5 style issues and context path issues

* organized WebSecurityConfig based on latest conventions and added basic support for oauth (more work needed)

* spotless applied

* added mock bean

* requires updates to properties - commented for now

* requires updates to properties - commented for now

* oauth secrets through env values

* user creation after oauth login

* integration test against non default context paths

* adjusted StartupMessage

* add global model element username

* conditionally show login oauth links

* fixed WebWolf login

---------

Co-authored-by: René Zubcevic <rene@Mac-mini-van-Rene.local>
This commit is contained in:
René Zubcevic
2023-11-14 10:01:59 +01:00
committed by GitHub
parent 5a4974f3c2
commit d1e44bbc98
114 changed files with 2763 additions and 546 deletions

View File

@ -242,6 +242,7 @@ public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(new UserInterceptor());
}
@Bean

View File

@ -0,0 +1,53 @@
package org.owasp.webgoat.container;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.owasp.webgoat.container.asciidoc.EnvironmentExposure;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class UserInterceptor implements HandlerInterceptor {
private Environment env = EnvironmentExposure.getEnv();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// Do nothing
return true;
}
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView)
throws Exception {
if (null != modelAndView) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (null != authentication) {
modelAndView.getModel().put("username", authentication.getName());
}
if (null != env) {
String githubClientId =
env.getProperty("spring.security.oauth2.client.registration.github.client-id");
if (null != githubClientId && !githubClientId.equals("dummy")) {
modelAndView.getModel().put("oauth", Boolean.TRUE);
}
} else {
modelAndView.getModel().put("oauth", Boolean.FALSE);
}
}
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// Do nothing
}
}

View File

@ -34,6 +34,9 @@ package org.owasp.webgoat.container;
import java.io.File;
import org.owasp.webgoat.container.session.UserSessionData;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.UserRepository;
import org.owasp.webgoat.container.users.WebGoatUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
@ -42,6 +45,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.web.client.RestTemplate;
@Configuration
@ -50,6 +55,8 @@ import org.springframework.web.client.RestTemplate;
@EnableAutoConfiguration
public class WebGoat {
@Autowired private UserRepository userRepository;
@Bean(name = "pluginTargetDirectory")
public File pluginTargetDirectory(@Value("${webgoat.user.directory}") final String webgoatHome) {
return new File(webgoatHome);
@ -58,7 +65,14 @@ public class WebGoat {
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public WebSession webSession() {
return new WebSession();
WebGoatUser webGoatUser = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof WebGoatUser) {
webGoatUser = (WebGoatUser) principal;
} else if (principal instanceof DefaultOAuth2User) {
webGoatUser = userRepository.findByUsername(((DefaultOAuth2User) principal).getName());
}
return new WebSession(webGoatUser);
}
@Bean

View File

@ -54,32 +54,41 @@ public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
auth ->
auth.requestMatchers(
"/css/**",
"/images/**",
"/js/**",
"fonts/**",
"/plugins/**",
"/registration",
"/register.mvc",
"/actuator/**")
.permitAll()
.anyRequest()
.authenticated());
http.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/welcome.mvc", true)
.usernameParameter("username")
.passwordParameter("password")
.permitAll();
http.logout().deleteCookies("JSESSIONID").invalidateHttpSession(true);
http.csrf().disable();
http.headers().cacheControl().disable();
http.exceptionHandling().authenticationEntryPoint(new AjaxAuthenticationEntryPoint("/login"));
return http.build();
return http.authorizeHttpRequests(
auth ->
auth.requestMatchers(
"/",
"/favicon.ico",
"/css/**",
"/images/**",
"/js/**",
"fonts/**",
"/plugins/**",
"/registration",
"/register.mvc")
.permitAll()
.anyRequest()
.authenticated())
.formLogin(
login ->
login
.loginPage("/login")
.defaultSuccessUrl("/welcome.mvc", true)
.usernameParameter("username")
.passwordParameter("password")
.permitAll())
.oauth2Login(
oidc -> {
oidc.defaultSuccessUrl("/login-oauth.mvc");
oidc.loginPage("/login");
})
.logout(logout -> logout.deleteCookies("JSESSIONID").invalidateHttpSession(true))
.csrf(csrf -> csrf.disable())
.headers(headers -> headers.disable())
.exceptionHandling(
handling ->
handling.authenticationEntryPoint(new AjaxAuthenticationEntryPoint("/login")))
.build();
}
@Autowired
@ -98,7 +107,6 @@ public class WebSecurityConfig {
return authenticationConfiguration.getAuthenticationManager();
}
@SuppressWarnings("deprecation")
@Bean
public NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();

View File

@ -16,7 +16,7 @@ public class EnvironmentExposure implements ApplicationContextAware {
private static ApplicationContext context;
public static Environment getEnv() {
return context.getEnvironment();
return (null != context) ? context.getEnvironment() : null;
}
@Override

View File

@ -3,7 +3,6 @@ package org.owasp.webgoat.container.session;
import java.io.Serializable;
import org.owasp.webgoat.container.lessons.Lesson;
import org.owasp.webgoat.container.users.WebGoatUser;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* *************************************************************************************************
@ -40,13 +39,12 @@ import org.springframework.security.core.context.SecurityContextHolder;
public class WebSession implements Serializable {
private static final long serialVersionUID = -4270066103101711560L;
private final WebGoatUser currentUser;
private WebGoatUser currentUser;
private transient Lesson currentLesson;
private boolean securityEnabled;
public WebSession() {
this.currentUser =
(WebGoatUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
public WebSession(WebGoatUser webGoatUser) {
this.currentUser = webGoatUser;
}
/**

View File

@ -3,8 +3,10 @@ package org.owasp.webgoat.container.users;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
@ -44,4 +46,12 @@ public class RegistrationController {
return "redirect:/attack";
}
@GetMapping("/login-oauth.mvc")
public String registrationOAUTH(Authentication authentication, HttpServletRequest request)
throws ServletException {
log.info("register oauth user in database");
userService.addUser(authentication.getName(), UUID.randomUUID().toString());
return "redirect:/welcome.mvc";
}
}

View File

@ -48,16 +48,19 @@ public class ResetLinkAssignmentForgotPassword extends AssignmentEndpoint {
private final RestTemplate restTemplate;
private String webWolfHost;
private String webWolfPort;
private String webWolfURL;
private final String webWolfMailURL;
public ResetLinkAssignmentForgotPassword(
RestTemplate restTemplate,
@Value("${webwolf.host}") String webWolfHost,
@Value("${webwolf.port}") String webWolfPort,
@Value("${webwolf.url}") String webWolfURL,
@Value("${webwolf.mail.url}") String webWolfMailURL) {
this.restTemplate = restTemplate;
this.webWolfHost = webWolfHost;
this.webWolfPort = webWolfPort;
this.webWolfURL = webWolfURL;
this.webWolfMailURL = webWolfMailURL;
}
@ -67,12 +70,12 @@ public class ResetLinkAssignmentForgotPassword extends AssignmentEndpoint {
@RequestParam String email, HttpServletRequest request) {
String resetLink = UUID.randomUUID().toString();
ResetLinkAssignment.resetLinks.add(resetLink);
String host = request.getHeader("host");
String host = request.getHeader(HttpHeaders.HOST);
if (ResetLinkAssignment.TOM_EMAIL.equals(email)
&& (host.contains(webWolfPort)
|| host.contains(webWolfHost))) { // User indeed changed the host header.
&& host.contains(webWolfHost))) { // User indeed changed the host header.
ResetLinkAssignment.userToTomResetLink.put(getWebSession().getUserName(), resetLink);
fakeClickingLinkEmail(host, resetLink);
fakeClickingLinkEmail(webWolfURL, resetLink);
} else {
try {
sendMailToUser(email, host, resetLink);
@ -97,13 +100,13 @@ public class ResetLinkAssignmentForgotPassword extends AssignmentEndpoint {
this.restTemplate.postForEntity(webWolfMailURL, mail, Object.class);
}
private void fakeClickingLinkEmail(String host, String resetLink) {
private void fakeClickingLinkEmail(String webWolfURL, String resetLink) {
try {
HttpHeaders httpHeaders = new HttpHeaders();
HttpEntity httpEntity = new HttpEntity(httpHeaders);
new RestTemplate()
.exchange(
String.format("http://%s/PasswordReset/reset/reset-password/%s", host, resetLink),
String.format("%s/PasswordReset/reset/reset-password/%s", webWolfURL, resetLink),
HttpMethod.GET,
httpEntity,
Void.class);

View File

@ -58,7 +58,8 @@ public class LandingAssignment extends AssignmentEndpoint {
public ModelAndView openPasswordReset(HttpServletRequest request) throws URISyntaxException {
URI uri = new URI(request.getRequestURL().toString());
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("webwolfUrl", landingPageUrl);
modelAndView.addObject(
"webwolfLandingPageUrl", landingPageUrl.replace("//landing", "/landing"));
modelAndView.addObject("uniqueCode", StringUtils.reverse(getWebSession().getUserName()));
modelAndView.setViewName("lessons/webwolfintroduction/templates/webwolfPasswordReset.html");

View File

@ -25,14 +25,12 @@
package org.owasp.webgoat.server;
import lombok.extern.slf4j.Slf4j;
import org.owasp.webgoat.container.WebGoat;
import org.owasp.webgoat.webwolf.WebWolf;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
@Slf4j
public class StartWebGoat {
public static void main(String[] args) {

View File

@ -15,16 +15,18 @@ public class StartupMessage {
private String port;
private String address;
private String contextPath;
@EventListener
void onStartup(ApplicationReadyEvent event) {
port = event.getApplicationContext().getEnvironment().getProperty("server.port");
address = event.getApplicationContext().getEnvironment().getProperty("server.address");
contextPath =
event.getApplicationContext().getEnvironment().getProperty("server.servlet.context-path");
if (StringUtils.hasText(port)
&& !StringUtils.hasText(System.getProperty("running.in.docker"))) {
log.info("Please browse to http://{}:{}/WebGoat to get started...", address, port);
}
if (event.getApplicationContext().getApplicationName().contains("WebGoat")) {
port = event.getApplicationContext().getEnvironment().getProperty("server.port");
address = event.getApplicationContext().getEnvironment().getProperty("server.address");
log.warn("Please browse to http://{}:{}{} to get started...", address, port, contextPath);
}
}

View File

@ -32,10 +32,9 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.owasp.webgoat.webwolf.user.WebGoatUser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
@ -58,6 +57,9 @@ public class FileServer {
@Value("${server.address}")
private String server;
@Value("${server.servlet.context-path}")
private String contextPath;
@Value("${server.port}")
private int port;
@ -71,9 +73,11 @@ public class FileServer {
}
@PostMapping(value = "/fileupload")
public ModelAndView importFile(@RequestParam("file") MultipartFile myFile) throws IOException {
var user = (WebGoatUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
var destinationDir = new File(fileLocation, user.getUsername());
public ModelAndView importFile(
@RequestParam("file") MultipartFile myFile, Authentication authentication)
throws IOException {
String username = authentication.getName();
var destinationDir = new File(fileLocation, username);
destinationDir.mkdirs();
myFile.transferTo(new File(destinationDir, myFile.getOriginalFilename()));
log.debug("File saved to {}", new File(destinationDir, myFile.getOriginalFilename()));
@ -92,15 +96,13 @@ public class FileServer {
}
@GetMapping(value = "/files")
public ModelAndView getFiles(HttpServletRequest request) {
WebGoatUser user =
(WebGoatUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = user.getUsername();
public ModelAndView getFiles(HttpServletRequest request, Authentication authentication) {
String username = (null != authentication) ? authentication.getName() : "anonymous";
File destinationDir = new File(fileLocation, username);
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("files");
File changeIndicatorFile = new File(destinationDir, user.getUsername() + "_changed");
File changeIndicatorFile = new File(destinationDir, username + "_changed");
if (changeIndicatorFile.exists()) {
modelAndView.addObject("uploadSuccess", request.getParameter("uploadSuccess"));
}
@ -117,7 +119,7 @@ public class FileServer {
}
modelAndView.addObject("files", uploadedFiles);
modelAndView.addObject("webwolf_url", "http://" + server + ":" + port);
modelAndView.addObject("webwolf_url", "http://" + server + ":" + port + contextPath);
return modelAndView;
}
}

View File

@ -24,8 +24,10 @@ package org.owasp.webgoat.webwolf;
import jakarta.annotation.PostConstruct;
import java.io.File;
import org.owasp.webgoat.container.UserInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -55,6 +57,12 @@ public class MvcConfiguration implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("webwolf-login");
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInterceptor());
}
@PostConstruct

View File

@ -22,6 +22,7 @@
package org.owasp.webgoat.webwolf;
import lombok.AllArgsConstructor;
import org.owasp.webgoat.container.AjaxAuthenticationEntryPoint;
import org.owasp.webgoat.webwolf.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -46,16 +47,39 @@ public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
auth -> auth.requestMatchers(HttpMethod.POST, "/fileupload").authenticated());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers(HttpMethod.GET, "/files", "/mail", "/requests").authenticated());
http.authorizeHttpRequests().anyRequest().permitAll();
http.csrf().disable().formLogin().loginPage("/login").failureUrl("/login?error=true");
http.formLogin().loginPage("/login").defaultSuccessUrl("/home", true).permitAll();
http.logout().permitAll();
return http.build();
return http.authorizeHttpRequests(
auth -> {
auth.requestMatchers("/css/**", "/webjars/**", "/favicon.ico", "/js/**", "/images/**")
.permitAll();
auth.requestMatchers(
HttpMethod.GET,
"/fileupload/**",
"/files/**",
"/landing/**",
"/PasswordReset/**")
.permitAll();
auth.requestMatchers(HttpMethod.POST, "/files", "/mail", "/requests").permitAll();
auth.anyRequest().authenticated();
})
.csrf(csrf -> csrf.disable())
.formLogin(
login ->
login
.loginPage("/login")
.failureUrl("/login?error=true")
.defaultSuccessUrl("/home", true)
.usernameParameter("username")
.passwordParameter("password")
.permitAll())
.oauth2Login(
oidc -> {
oidc.defaultSuccessUrl("/home");
})
.logout(logout -> logout.deleteCookies("WEBWOLFSESSION").invalidateHttpSession(true))
.exceptionHandling(
handling ->
handling.authenticationEntryPoint(new AjaxAuthenticationEntryPoint("/login")))
.build();
}
@Autowired

View File

@ -25,8 +25,8 @@ package org.owasp.webgoat.webwolf.mailbox;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.Authentication;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@ -42,16 +42,16 @@ public class MailboxController {
private final MailboxRepository mailboxRepository;
@GetMapping("/mail")
public ModelAndView mail() {
UserDetails user =
(UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
public ModelAndView mail(Authentication authentication, Model model) {
String username = (null != authentication) ? authentication.getName() : "anonymous";
ModelAndView modelAndView = new ModelAndView();
List<Email> emails = mailboxRepository.findByRecipientOrderByTimeDesc(user.getUsername());
List<Email> emails = mailboxRepository.findByRecipientOrderByTimeDesc(username);
if (emails != null && !emails.isEmpty()) {
modelAndView.addObject("total", emails.size());
modelAndView.addObject("emails", emails);
}
modelAndView.setViewName("mailbox");
model.addAttribute("username", username);
return modelAndView;
}

View File

@ -33,8 +33,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -64,12 +63,12 @@ public class Requests {
}
@GetMapping
public ModelAndView get() {
public ModelAndView get(Authentication authentication) {
var model = new ModelAndView("requests");
var user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = (null != authentication) ? authentication.getName() : "anonymous";
var traces =
traceRepository.findAllTraces().stream()
.filter(t -> allowedTrace(t, user))
.filter(t -> allowedTrace(t, username))
.map(t -> new Tracert(t.getTimestamp(), path(t), toJsonString(t)))
.collect(toList());
model.addObject("traces", traces);
@ -77,17 +76,16 @@ public class Requests {
return model;
}
private boolean allowedTrace(HttpExchange t, UserDetails user) {
private boolean allowedTrace(HttpExchange t, String username) {
HttpExchange.Request req = t.getRequest();
boolean allowed = true;
/* do not show certain traces to other users in a classroom setup */
if (req.getUri().getPath().contains("/files")
&& !req.getUri().getPath().contains(user.getUsername())) {
if (req.getUri().getPath().contains("/files") && !req.getUri().getPath().contains(username)) {
allowed = false;
} else if (req.getUri().getPath().contains("/landing")
&& req.getUri().getQuery() != null
&& req.getUri().getQuery().contains("uniqueCode")
&& !req.getUri().getQuery().contains(StringUtils.reverse(user.getUsername()))) {
&& !req.getUri().getQuery().contains(StringUtils.reverse(username))) {
allowed = false;
}

View File

@ -25,7 +25,6 @@ package org.owasp.webgoat.webwolf.requests;
import com.google.common.collect.EvictingQueue;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
@ -36,7 +35,6 @@ import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
* @author nbaars
* @since 8/13/17.
*/
@Slf4j
public class WebWolfTraceRepository implements HttpExchangeRepository {
private final EvictingQueue<HttpExchange> traces = EvictingQueue.create(10000);
@ -46,7 +44,6 @@ public class WebWolfTraceRepository implements HttpExchangeRepository {
"/home",
"/files",
"/images/",
"/favicon.ico",
"/js/",
"/webjars/",
"/requests",