Notes start at the first OWASP Top 10 exercise: SQL Injection
- The solution for the SQL Injection is:
public List<AccountDTO> findAccountsByCustomerIdPS(String customerId) throws SQLException {
String jql = "select * from Account where customer_id=?";
final PreparedStatement preparedStatement = dataSource.getConnection().prepareStatement(jql);
preparedStatement.setString(1, customerId);
ResultSet rs = preparedStatement.executeQuery();
List<AccountDTO> accounts = new ArrayList<>();
while (rs.next()) {
AccountDTO acc = AccountDTO.builder()
.id(rs.getLong("id"))
.customerId(rs.getString("customer_id"))
.name(rs.getString("name"))
.balance(rs.getLong("balance"))
.build();
accounts.add(acc);
}
return accounts;
}
-
In Broken Access Control mention these:
- ACL, ABAC, RBAC (and also that we'll talk about these later)
-
Things to talk about the Misconfiguration solution:
- Use the proper
application.properties
- Check for potential hacks in the file upload (
..
for example) - Check file type
- Check mime type
- Use transformers that would fail if uploaded file was not the correct type
- Use the proper
-
In XSS, use this input first:
<b onmouseover=alert('Wufff!')>click me!</b>
- Then use an image:
<img src="https://images.dog.ceo/breeds/retriever-golden/n02099601_2663.jpg" />
- Then steal cookies:
<script>console.log("Document:" + document.cookie);</script>
- Then use an image:
-
In Deserialization create a
UserDTO
that only holds the acceptable fields and map it to aUser
object. -
In Known Vulnerabilities
- run
./mvnw clean install -Powasp-dependency-check
- Show NVD (National Vulnerability Database)
- Search for Spring
- Mention Kyro that's on the list (with regards to deserialization)
- Addd spring cloud config:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
- run
-
In CORS
- Start the
cors-server
andcors-client
projects - try loading
localhost:9090/simple-cors
- You will get an error
- Solution is:
@CrossOrigin(origins = "http://localhost:9090")
- Start the
-
In Clickjacking
-
In Form Tampering
- Use:
<button type="submit" form="profile-form" formaction="http://localhost:8080/steal-your-data" style="position:relative;top:94px;left:28px;" type="submit" >Update</button>
-
In
ReadJavaHome
the solution is:File policy = new File("src/main/resources/home.policy"); System.setProperty("java.security.policy", policy.getAbsolutePath()); System.setSecurityManager(new SecurityManager()); System.out.printf("java.home is : %s%n", System.getProperty("java.home"));
Add these to pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Create WebSecurityConfig
in the config
package:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
Talk about how configure
works and how UserDetailsService
works.
Then craete the login
page.
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
>
<head>
<title>Spring Security Example </title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div class="container mt-2">
<div th:if="${param.error}" class="alert alert-danger">
Invalid username and password.
</div>
<div th:if="${param.logout}" class="alert alert-success">
You have been logged out.
</div>
<form th:action="@{/login}" method="post" class="card">
<h5 class="card-header">
Log In
</h5>
<div class="card-body">
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" class="form-control"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control"/>
</div>
<button class="btn btn-primary" type="submit">Go</button>
</div>
</form>
</div>
</body>
</html>
and modify the hello.html
to include a logout form:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" class="btn btn-primary"/>
</form>
</div>
</body>
</html>
Try it out! (http://localhost:8080/home
)
Tell them that this works because spring has a default implementation.
Show them the injected CSRF token and the headers that are added by default.
Now let's look at the password encoder. It is not secure! Reimplement it using our own implementation (Argon2):
@Bean
public Argon2PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder();
}
// ...
UserDetails user = User.builder()
.username("user")
.password("password")
.roles("USER")
.passwordEncoder(passwordEncoder()::encode)
.build();
Talk about why this is necessary:
- Plain text passwords
- hashed passwords > rainbow tables
- salted passwords > brute force
- state of the art: slow encoders like argon1
- corollaray: we need tokens so that passwords are only checked once
Show them what User
, UserDetails
and UserDetailsService
is!
Talk about how to traverse the source code (Ctrl + Click and Download Sources).
Now look at the log after running ./mvnw spring-boot:run
.
Talk about all the filters that are present.
Now let's add a new implementation of DaoAuthenticationProvider
:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
private static Logger LOGGER = LoggerFactory.getLogger(MyDaoAuthenticationProvider.class);
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
LOGGER.info("Trying to authenticate with: {}", authentication);
return super.authenticate(authentication);
}
@Override
public boolean supports(Class<?> authentication) {
LOGGER.info("Checking if supports with: {}", authentication);
return super.supports(authentication);
}
}
Use it from the security config:
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new MyDaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userDetailsService());
return provider;
}
Restart the application and see the log messages. Talk about how this can be further expanded into a database-backed authentication method.
Talk about how Spring automatically protects against session fixation by creating an new
JSESSIONID
whenever it is necessary.
Add this as a maven dependency:
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Replace the previous hello message with:
<h1 sec:authorize="isAuthenticated()">
Hello, <span sec:authentication="name"></span>
</h1>
Restart the app, and see the result.
Modify the @EnableWebSecurity
to @EnableWebSecurity(debug = true)
.
Restart the application and see the result.
Restore the original version.
Add this to application.yml
:
logging:
level:
org:
springframework:
security:
web:
FilterChainProxy: DEBUG
See the result.
Restore the original.
Add a new user with an additional ADMIN
role:
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("password")
.roles("USER")
.passwordEncoder(passwordEncoder()::encode)
.build();
UserDetails admin = User.builder()
.username("admin")
.password("admin")
.roles("USER", "ADMIN")
.passwordEncoder(passwordEncoder()::encode)
.build();
return new InMemoryUserDetailsManager(user, admin);
}
Create a new Controller
that renders an admin page:
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AdminController {
@GetMapping("/admin")
@Secured("ROLE_ADMIN")
public String admin() {
return "admin";
}
}
Enable global method security by creating this new config:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig
extends GlobalMethodSecurityConfiguration {
}
Implement the admin page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:th="https://www.thymeleaf.org">
<head>
<title>Admin</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div class="container mt-2">
<div class="card">
<h5 class="card-header">Hello, <span sec:authentication="name"></span></h5>
<div class="card-body">
You're on the admin page!
Go <a th:href="@{/home}">Home</a>.
</div>
</div>
</div>
</body>
</html>
See how it works for users and admins. Users get an exception. This is not good.
Create an AccessDeniedHandler
:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
public static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e
) throws IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
LOGGER.warn("User: {} attempted to access the protected URL: {}",
auth.getName(), request.getRequestURI());
}
response.sendRedirect(request.getContextPath() + "/accessDenied");
}
}
Add it to the security config as a @Bean
:
@Bean
public AccessDeniedHandler accessDeniedHandler(){
return new CustomAccessDeniedHandler();
}
Add an access denied page to the security config:
.exceptionHandling()
.accessDeniedPage("/access-denied")
.and()
And add the corresponding view, access-denied.html
:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>You don't have access to this page!</h1>
<p>Go <a th:href="@{/home}">home</a>.</p>
</div>
</body>
</html>
And finally the view registration in MvcConfig
:
registry.addViewController("/access-denied").setViewName("access-denied");
Change the formLogin
in the security config to this:
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/hello", true)
.permitAll()
.and()
Show them how this works.
Now create a success manager (and explain it):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final Map<String, String> targetLookup = new HashMap<>();
{
targetLookup.put("ROLE_USER", "/hello");
targetLookup.put("ROLE_ADMIN", "/admin");
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication
) throws IOException {
handle(request, response, authentication);
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
handle(request, response, authentication);
}
protected void handle(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
LOGGER.debug("Response has already been committed. Unable to redirect to {}", targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(final Authentication authentication) {
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (final GrantedAuthority grantedAuthority : authorities) {
String authorityName = grantedAuthority.getAuthority();
if (targetLookup.containsKey(authorityName)) {
return targetLookup.get(authorityName);
}
}
throw new IllegalStateException();
}
}
Add a success manager to the security config:
@Bean
public AuthenticationSuccessHandler customAuthenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler();
}
And add change the successUrl
to a successHandler
in the formLogin
section:
successHandler(customAuthenticationSuccessHandler())
Profit.
Add a token repository:
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new InMemoryTokenRepositoryImpl();
}
Add remember me config to the security config:
.rememberMe()
.tokenRepository(persistentTokenRepository())
.rememberMeParameter("remember-me")
.and()
then add this checkbox to the login form:
<div class="form-check">
<input type="checkbox" name="remember-me" id="remember-me" class="form-check-input"/>
<label for="remember-me" class="form-check-label">Remember Me</label>
</div>
Add the OAuth dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Create a new OAuth app on GitHub here.
Homepage: http://localhost:8080
CallbackURL: http://localhost:8080/login/oauth2/code/github
Add the Client ID and Secret to application.yml
(use env vars!!):
spring:
security:
oauth2:
client:
registration:
github:
clientId: github-client-id
clientSecret: github-client-secret
Enable OAuth login in the security config:
.oauth2Login()
.and()
Disable form login.
Show how this works...
Then tell them that a complete configuration of OAuth is out of the scope of this training as it is very complex...undo the changes.
Show the simple config:
.cors(withDefaults())
Then show the fine-grained version:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:9090"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Add "/api/**"
to the permitAll
part.
And add the /api/simple-cors
endpoint:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CORSController {
@GetMapping("/api/simple-cors")
public String simpleCors() {
return "ok";
}
}
Now open up the cors-client
program (we used it earlier), start it and navigate to
http://localhost:9090/simple-cors
and look at the logs. It should work.
Now comment out the cors config @Bean
and try it again. It won't work.
Create a login attempt service:
public class LoginAttemptService {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginAttemptService.class);
private static final AtomicInteger DEFAULT_COUNTER = new AtomicInteger();
private final int MAX_ATTEMPTS = 5;
private final ConcurrentMap<String, AtomicInteger> attemptsLookup = new ConcurrentHashMap<>();
public void loginSucceeded(String key) {
attemptsLookup.remove(key);
}
public void loginFailed(String key) {
attemptsLookup.putIfAbsent(key, new AtomicInteger());
AtomicInteger attempts = attemptsLookup.get(key);
attempts.incrementAndGet();
LOGGER.info("Login failed for key: {}. Attempts: {}}", key, attempts.get());
}
public boolean isBlocked(String key) {
return attemptsLookup.getOrDefault(key,DEFAULT_COUNTER).get() >= MAX_ATTEMPTS;
}
}
Add it as a bean to the security config:
@Bean
public LoginAttemptService loginAttemptService() {
return new LoginAttemptService();
}
Add an authentication failure listener:
@Component
public class AuthenticationFailureListener
implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
private final LoginAttemptService loginAttemptService;
public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
WebAuthenticationDetails auth = (WebAuthenticationDetails)
e.getAuthentication().getDetails();
loginAttemptService.loginFailed(auth.getRemoteAddress());
}
}
Tell them why we need to track the IP, and not the user (they can attempt with any user).
Now modify the success handler to notify the LoginAttemptService
:
private LoginAttemptService loginAttemptService;
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
fix the security config:
@Bean
public AuthenticationSuccessHandler customAuthenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler(loginAttemptService());
}
and notify the LoginAttemptService
:
loginAttemptService.loginSucceeded(request.getRemoteAddr());
Create a new exception for the ban event:
public class YouAreABadPersonException extends AuthenticationException {
public YouAreABadPersonException(String msg) {
super(msg);
}
}
Modify the MyDaoAuthenticationProvider
to take attempts into account:
private LoginAttemptService loginAttemptService;
public MyDaoAuthenticationProvider(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
if(loginAttemptService.isBlocked(request.getRemoteAddr())) {
throw new YouAreABadPersonException("And you are banned");
}
}
LOGGER.info("Trying to authenticate with: {}", authentication);
return super.authenticate(authentication);
}
and fix the security config:
DaoAuthenticationProvider provider = new MyDaoAuthenticationProvider(loginAttemptService());
Then add a custom error handler for our exception:
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class BadPersonHandler implements AuthenticationFailureHandler {
private final AuthenticationFailureHandler delegate = new SimpleUrlAuthenticationFailureHandler(
"/login?error");
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception
) throws IOException, ServletException {
if(exception instanceof YouAreABadPersonException) {
response.getWriter().write("You are a bad person");
} else {
delegate.onAuthenticationFailure(request, response, exception);
}
}
}
and reconfigure security config with this:
@Bean
public AuthenticationFailureHandler badPersonHandler() {
return new BadPersonHandler();
}
// ...
.failureHandler(badPersonHandler())