diff --git a/pom.xml b/pom.xml index 6a66732c..484be4eb 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,8 @@ 1.8 + 1.19.0 + 4.10.0 @@ -172,6 +174,57 @@ 1.0.0 + + + com.redis.testcontainers + testcontainers-redis + 1.6.4 + test + + + org.zaproxy + zap-clientapi + 1.12.0 + test + + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + test + + + org.seleniumhq.selenium + selenium-firefox-driver + ${selenium.version} + test + + + org.asynchttpclient + async-http-client + 2.12.3 + test + + + com.google.guava + guava + 31.1-jre + test + + + org.testng + testng + 7.7.1 + test + + + io.rest-assured + rest-assured + 5.3.1 + test + + + diff --git a/src/main/java/com/nonononoki/alovoa/component/AuthFilter.java b/src/main/java/com/nonononoki/alovoa/component/AuthFilter.java index 29d806b6..be5314b6 100644 --- a/src/main/java/com/nonononoki/alovoa/component/AuthFilter.java +++ b/src/main/java/com/nonononoki/alovoa/component/AuthFilter.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -12,24 +13,37 @@ public class AuthFilter extends UsernamePasswordAuthenticationFilter { - private static final String USERNAME = "username"; - private static final String PASSWORD = "password"; - private static final String CAPTCHA_ID = "captchaId"; - private static final String CAPTCHA_TEXT = "captchaText"; - public static final String REDIRECT_URL = "redirect-url"; - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException { - - String username = request.getParameter(USERNAME); - String password = request.getParameter(PASSWORD); - long captchaId = Long.parseLong(request.getParameter(CAPTCHA_ID)); - String captchaText = request.getParameter(CAPTCHA_TEXT); - request.getSession().setAttribute(REDIRECT_URL, request.getParameter(REDIRECT_URL)); - - AuthToken auth = new AuthToken(username, password, captchaId, captchaText); - AuthenticationManager am = this.getAuthenticationManager(); - return am.authenticate(auth); - } + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String CAPTCHA_ID = "captchaId"; + private static final String CAPTCHA_TEXT = "captchaText"; + public static final String REDIRECT_URL = "redirect-url"; + + @Value("${app.captcha.login.enabled}") + private String captchaLoginEnabled; + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { + long captchaId; + String captchaText; + + String username = request.getParameter(USERNAME); + String password = request.getParameter(PASSWORD); + + if (Boolean.parseBoolean(captchaLoginEnabled)) { + captchaId = Long.parseLong(request.getParameter(CAPTCHA_ID)); + captchaText = request.getParameter(CAPTCHA_TEXT); + } else { + captchaId = -1; + captchaText = null; + } + + request.getSession().setAttribute(REDIRECT_URL, request.getParameter(REDIRECT_URL)); + + AuthToken auth = new AuthToken(username, password, captchaId, captchaText); + AuthenticationManager am = this.getAuthenticationManager(); + return am.authenticate(auth); + + } } diff --git a/src/main/java/com/nonononoki/alovoa/component/AuthProvider.java b/src/main/java/com/nonononoki/alovoa/component/AuthProvider.java index c65019e3..278775aa 100644 --- a/src/main/java/com/nonononoki/alovoa/component/AuthProvider.java +++ b/src/main/java/com/nonononoki/alovoa/component/AuthProvider.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; @@ -37,7 +38,10 @@ public class AuthProvider implements AuthenticationProvider { @Autowired private PasswordEncoder passwordEncoder; - + + @Value("${app.captcha.login.enabled}") + private String captchaEnabled; + @SuppressWarnings("unused") private static final Logger logger = LoggerFactory.getLogger(AuthProvider.class); @@ -50,15 +54,20 @@ public Authentication authenticate(Authentication authentication) throws Authent long captchaId = a.getCaptchaId(); String captchaText = a.getCaptchaText(); - Captcha c = captchaRepo.findById(captchaId).orElse(null); - if (c == null) { - throw new BadCredentialsException(""); - } + if (Boolean.parseBoolean(captchaEnabled)) { + logger.debug("Captcha enabled, so check captcha"); + Captcha c = captchaRepo.findById(captchaId).orElse(null); + if (c == null) { + throw new BadCredentialsException(""); + } - captchaRepo.delete(c); + captchaRepo.delete(c); - if (!c.getText().equalsIgnoreCase(captchaText)) { - throw new BadCredentialsException(""); + if (!c.getText().equalsIgnoreCase(captchaText)) { + throw new BadCredentialsException(""); + } + } else { + logger.debug("Captcha disabled, so we do not care about it"); } User user = userRepo.findByEmail(email); diff --git a/src/main/java/com/nonononoki/alovoa/html/DeleteAccountResource.java b/src/main/java/com/nonononoki/alovoa/html/DeleteAccountResource.java index 3bce4cb4..fecacb80 100644 --- a/src/main/java/com/nonononoki/alovoa/html/DeleteAccountResource.java +++ b/src/main/java/com/nonononoki/alovoa/html/DeleteAccountResource.java @@ -40,6 +40,9 @@ public class DeleteAccountResource { @Value("${app.user.delete.duration.valid}") private long accountDeleteDuration; + @Value("${app.captcha.delete.enabled}") + private String captchaDeleteEnabled; + @GetMapping("/delete-account/{tokenString}") public ModelAndView deleteAccount(@PathVariable String tokenString) throws AlovoaException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, @@ -57,6 +60,7 @@ public ModelAndView deleteAccount(@PathVariable String tokenString) throws Alovo active = true; } mav.addObject("active", active); + mav.addObject("captchaEnabled", Boolean.valueOf(captchaDeleteEnabled)); return mav; } diff --git a/src/main/java/com/nonononoki/alovoa/html/ImprintResource.java b/src/main/java/com/nonononoki/alovoa/html/ImprintResource.java index 838bc1e7..626da989 100644 --- a/src/main/java/com/nonononoki/alovoa/html/ImprintResource.java +++ b/src/main/java/com/nonononoki/alovoa/html/ImprintResource.java @@ -11,11 +11,16 @@ public class ImprintResource { @Value("${app.company.name}") private String companyName; + @Value("${app.captcha.imprint.enabled}") + private String captchaImprintEnabled; + @GetMapping("/imprint") public ModelAndView imprint() { ModelAndView mav = new ModelAndView("imprint"); mav.addObject("companyName", companyName); + mav.addObject("captchaEnabled", Boolean.valueOf(captchaImprintEnabled)); + return mav; } } diff --git a/src/main/java/com/nonononoki/alovoa/html/LoginResource.java b/src/main/java/com/nonononoki/alovoa/html/LoginResource.java index d03af09c..e0887693 100644 --- a/src/main/java/com/nonononoki/alovoa/html/LoginResource.java +++ b/src/main/java/com/nonononoki/alovoa/html/LoginResource.java @@ -18,7 +18,10 @@ public class LoginResource { @Value("${app.privacy.update-date}") private String privacyDate; - + + @Value("${app.captcha.login.enabled}") + private String captchaEnabled; + public static final String URL = "/login"; @GetMapping(URL) @@ -30,6 +33,7 @@ public ModelAndView login() throws AlovoaException { } ModelAndView mav = new ModelAndView("login"); + mav.addObject("captchaEnabled", Boolean.valueOf(captchaEnabled)); return mav; } } diff --git a/src/main/java/com/nonononoki/alovoa/html/PasswordResource.java b/src/main/java/com/nonononoki/alovoa/html/PasswordResource.java index ff2ca606..e00850cd 100644 --- a/src/main/java/com/nonononoki/alovoa/html/PasswordResource.java +++ b/src/main/java/com/nonononoki/alovoa/html/PasswordResource.java @@ -7,6 +7,7 @@ import com.nonononoki.alovoa.service.AuthService; import com.nonononoki.alovoa.service.UserService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -34,6 +35,9 @@ public class PasswordResource { @Autowired private TextEncryptorConverter textEncryptor; + @Value("${app.captcha.password.enabled}") + private String captchaPasswordEnabled; + @GetMapping("/reset") public ModelAndView passwordReset() throws AlovoaException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, @@ -41,6 +45,7 @@ public ModelAndView passwordReset() throws AlovoaException, InvalidKeyException, ModelAndView mav = new ModelAndView("password-reset"); User user = authService.getCurrentUser(); mav.addObject("user", UserDto.userToUserDto(user, user, userService, textEncryptor, UserDto.NO_MEDIA)); + mav.addObject("captchaEnabled", Boolean.valueOf(captchaPasswordEnabled)); return mav; } diff --git a/src/main/java/com/nonononoki/alovoa/html/RegisterResource.java b/src/main/java/com/nonononoki/alovoa/html/RegisterResource.java index a3809a21..4694f26c 100644 --- a/src/main/java/com/nonononoki/alovoa/html/RegisterResource.java +++ b/src/main/java/com/nonononoki/alovoa/html/RegisterResource.java @@ -1,6 +1,7 @@ package com.nonononoki.alovoa.html; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -27,7 +28,10 @@ public class RegisterResource { @Autowired private AuthService authService; - + + @Value("${app.captcha.register.enabled}") + private String captchaRegisterEnabled; + public static final String URL = "/register"; @GetMapping(URL) @@ -40,6 +44,7 @@ public ModelAndView register() throws AlovoaException { ModelAndView mav = new ModelAndView("register"); mav.addObject("genders", genderRepo.findAll()); mav.addObject("intentions", userIntentionRepo.findAll()); + mav.addObject("captchaRegisterEnabled", Boolean.parseBoolean(captchaRegisterEnabled)); return mav; } diff --git a/src/main/java/com/nonononoki/alovoa/model/RegisterDto.java b/src/main/java/com/nonononoki/alovoa/model/RegisterDto.java index 0c70e258..ee5d9561 100644 --- a/src/main/java/com/nonononoki/alovoa/model/RegisterDto.java +++ b/src/main/java/com/nonononoki/alovoa/model/RegisterDto.java @@ -17,4 +17,7 @@ public class RegisterDto { private boolean termsConditions; private boolean privacy; + + private String captchaText; + private long captchaId; } diff --git a/src/main/java/com/nonononoki/alovoa/service/ImprintService.java b/src/main/java/com/nonononoki/alovoa/service/ImprintService.java index fad2e414..b22596a3 100644 --- a/src/main/java/com/nonononoki/alovoa/service/ImprintService.java +++ b/src/main/java/com/nonononoki/alovoa/service/ImprintService.java @@ -5,6 +5,7 @@ import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.nonononoki.alovoa.entity.Contact; @@ -24,11 +25,16 @@ public class ImprintService { @Autowired private ContactRepository contactRepo; + @Value("${app.captcha.imprint.enabled}") + private String captchaImprintEnabled; + public Contact contact(ContactDto dto) throws UnsupportedEncodingException, NoSuchAlgorithmException, AlovoaException { - boolean isValid = captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText()); - if (!isValid) { - throw new AlovoaException(publicService.text("backend.error.captcha.invalid")); + if (Boolean.parseBoolean(captchaImprintEnabled)) { + boolean isValid = captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText()); + if (!isValid) { + throw new AlovoaException(publicService.text("backend.error.captcha.invalid")); + } } Contact c = new Contact(); diff --git a/src/main/java/com/nonononoki/alovoa/service/PasswordService.java b/src/main/java/com/nonononoki/alovoa/service/PasswordService.java index 0eb86352..0f329fe4 100644 --- a/src/main/java/com/nonononoki/alovoa/service/PasswordService.java +++ b/src/main/java/com/nonononoki/alovoa/service/PasswordService.java @@ -52,14 +52,19 @@ public class PasswordService { @Value("${app.user.password-reset.duration.valid}") private int userPasswordResetDuration; + @Value("${app.captcha.password.enabled}") + private String captchaPasswordEnabled; + public UserPasswordToken resetPassword(PasswordResetDto dto) throws AlovoaException, NoSuchAlgorithmException, MessagingException, IOException { User u = authService.getCurrentUser(); if (u == null) { - if (!captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText())) { - throw new AlovoaException("captcha_invalid"); + if (Boolean.parseBoolean(captchaPasswordEnabled)) { + if (!captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText())) { + throw new AlovoaException("captcha_invalid"); + } } u = userRepo.findByEmail(Tools.cleanEmail(dto.getEmail())); diff --git a/src/main/java/com/nonononoki/alovoa/service/RegisterService.java b/src/main/java/com/nonononoki/alovoa/service/RegisterService.java index b5af5ef3..c1d67aff 100644 --- a/src/main/java/com/nonononoki/alovoa/service/RegisterService.java +++ b/src/main/java/com/nonononoki/alovoa/service/RegisterService.java @@ -108,6 +108,9 @@ public class RegisterService { @Autowired private TextEncryptorConverter textEncryptor; + @Value("${app.captcha.register.enabled}") + private String captchaRegisterEnabled; + private static final int MIN_PASSWORD_SIZE = 7; private static final Logger logger = LoggerFactory.getLogger(RegisterService.class); @@ -121,6 +124,13 @@ public String register(RegisterDto dto) throw new AlovoaException("email_invalid"); } + if (Boolean.parseBoolean(captchaRegisterEnabled)) { + boolean isValid = captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText()); + if (!isValid) { + throw new AlovoaException(publicService.text("backend.error.captcha.invalid")); + } + } + if (!profile.equals(Tools.DEV)) { dto.setEmail(Tools.cleanEmail(dto.getEmail())); if (plusAddressing && dto.getEmail().contains("+")) { diff --git a/src/main/java/com/nonononoki/alovoa/service/UserService.java b/src/main/java/com/nonononoki/alovoa/service/UserService.java index 5952d065..5fb31837 100644 --- a/src/main/java/com/nonononoki/alovoa/service/UserService.java +++ b/src/main/java/com/nonononoki/alovoa/service/UserService.java @@ -120,6 +120,9 @@ public class UserService { @Value("${app.intention.delay}") private long intentionDelay; + @Value("${app.captcha.delete.enabled}") + private String captchaDeleteEnabled; + public static void removeUserDataCascading(User user, UserDeleteParams userDeleteParam) { UserRepository userRepo = userDeleteParam.getUserRepo(); @@ -329,8 +332,10 @@ public void deleteAccountConfirm(UserDeleteAccountDto dto) throw new AlovoaException("deletion_wrong_email"); } - if (!captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText())) { - throw new AlovoaException("captcha_invalid"); + if (Boolean.parseBoolean(captchaDeleteEnabled)) { + if (!captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText())) { + throw new AlovoaException("captcha_invalid"); + } } UserDeleteParams userDeleteParam = UserDeleteParams.builder().conversationRepo(conversationRepo) @@ -800,7 +805,7 @@ public boolean hasNewAlert(String lang) throws AlovoaException { User user = authService.getCurrentUser(true); // user always check their alerts periodically in the background, so just update // it here - if (user != null) { + if (user != null && user.getDates()!=null) { updateUserInfo(user, lang); return user.getDates().getNotificationDate().after(user.getDates().getNotificationCheckedDate()); } else { @@ -810,7 +815,7 @@ public boolean hasNewAlert(String lang) throws AlovoaException { public boolean hasNewMessage() throws AlovoaException { User user = authService.getCurrentUser(true); - if (user != null && user.getDates().getMessageDate() != null + if (user != null && user.getDates()!=null && user.getDates().getMessageDate() != null && user.getDates().getMessageCheckedDate() != null) { return user.getDates().getMessageDate().after(user.getDates().getMessageCheckedDate()); } else { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 39f8c22a..1f3d650f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,6 +2,15 @@ spring.profiles.active=test server.port=${PORT:8080} +server.forward-headers-strategy=native +server.tomcat.remote-ip-header=x-forwarded-for +server.tomcat.protocol-header=x-forwarded-proto + +app.captcha.login.enabled=false +app.captcha.delete.enabled=false +app.captcha.imprint.enabled=true +app.captcha.password.enabled=false +app.captcha.register.enabled=true #enable to force https for development #server.ssl.key-store-type=PKCS12 diff --git a/src/main/resources/templates/delete-account.html b/src/main/resources/templates/delete-account.html index 0f98e379..c954a717 100644 --- a/src/main/resources/templates/delete-account.html +++ b/src/main/resources/templates/delete-account.html @@ -38,7 +38,9 @@

-
+
+
+
@@ -68,7 +68,7 @@ @@ -912,4 +912,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/templates/imprint.html b/src/main/resources/templates/imprint.html index c337a62c..b85c1e37 100644 --- a/src/main/resources/templates/imprint.html +++ b/src/main/resources/templates/imprint.html @@ -48,7 +48,9 @@

th:placeholder="#{message}" required>
-
+
+
+
@@ -77,4 +79,4 @@

- \ No newline at end of file + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 1baa36f8..d983b992 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -60,13 +60,15 @@

name="password"> -
+
+
+
- + @@ -117,4 +119,4 @@

- \ No newline at end of file + diff --git a/src/main/resources/templates/password-reset.html b/src/main/resources/templates/password-reset.html index 69b18b38..9ad23eb4 100644 --- a/src/main/resources/templates/password-reset.html +++ b/src/main/resources/templates/password-reset.html @@ -31,7 +31,9 @@

-
+
+
+
@@ -53,4 +55,4 @@

- \ No newline at end of file + diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html index 860ce36b..fc7b8cd7 100644 --- a/src/main/resources/templates/register.html +++ b/src/main/resources/templates/register.html @@ -142,7 +142,11 @@

th:utext="#{register.privacy-agree}"> - + +
+
+
+
@@ -166,6 +170,7 @@

+ @@ -174,4 +179,4 @@

- \ No newline at end of file + diff --git a/src/test/java/com/nonononoki/alovoa/dast/OwaspZapTest.java b/src/test/java/com/nonononoki/alovoa/dast/OwaspZapTest.java new file mode 100644 index 00000000..ee9e439f --- /dev/null +++ b/src/test/java/com/nonononoki/alovoa/dast/OwaspZapTest.java @@ -0,0 +1,223 @@ +package com.nonononoki.alovoa.dast; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Proxy; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeDriverService; +import org.openqa.selenium.chrome.ChromeOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; +import org.zaproxy.clientapi.core.*; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ActiveProfiles("test") +@TestPropertySource("classpath:application-dev.properties") +@Transactional +public class OwaspZapTest { + private static final Logger logger = LoggerFactory.getLogger(OwaspZapTest.class); + + private static final String ZAP_ADDRESS = "localhost"; + private static final String ZAP_API_KEY = "NZax2DPZt41UmLTeHi4nqYUSIPGdFIGw"; + private static ClientApi api; + + public static final DockerImageName OWASPZAP_IMAGE = DockerImageName.parse("softwaresecurityproject/zap-stable:latest"); + + private static final int OWASP_PORT = 8080; + + private static final List zapEnv = new ArrayList<>(); + + @Value("${app.admin.email}") + private String EMAIL; + + @Value("${app.admin.key}") + private String PASSWORD; + + @Value("${app.domain}") + private String appDomain; + + + // Beware of Zap is running under Docker. Real report path is ./target/test-classes/zap-report.html + private static final String DOCKER_REPORT_PATH = "/tmp/reports"; + + private static ChromeDriver chromeDriver; + + @LocalServerPort + private int alovoaPort; + + + private static void executeSpiderScan(String targetUrl) { + System.out.println("Spidering target : " + targetUrl); + String scanId; + + try { + int progress; + ApiResponse response = api.spider.scan(targetUrl, null, null, null, null); + scanId = ((ApiResponseElement) response).getValue(); + do { + Thread.sleep(1000); + progress = Integer.parseInt(((ApiResponseElement) api.spider.status(scanId)).getValue()); + logger.info(String.format("Spider progress : %s %%", progress)); + } while (progress < 100); + logger.info("Spider completed!"); + List spiderResults = ((ApiResponseList) api.spider.results(scanId)).getItems(); + logger.info("Following resources have been found:"); + spiderResults.forEach(System.out::println); + } catch (Exception e) { + logger.error(String.format("%s", e.getMessage())); + } + } + + public static void executeActiveScan(String targetUrl) { + logger.info(String.format("Active scanning target : %s", targetUrl)); + String scanId; + try { + int progress; + ApiResponse response = api.ascan.scan(targetUrl, + "True", + "False", + null, + null, + null + ); + scanId = ((ApiResponseElement) response).getValue(); + do { + Thread.sleep(5000); + progress = Integer.parseInt(((ApiResponseElement) api.ascan.status(scanId)).getValue()); + logger.info(String.format("Active scan progress : %s %%", progress)); + } while (progress < 100); + logger.info("Active scan completed!"); + } catch (ClientApiException | InterruptedException e) { + logger.error(String.format("Exception caught: %s", e.getMessage())); + } + } + + @BeforeAll + public static void setup() throws ClientApiException { + zapEnv.add(String.format("ZAP_PORT=%s", OWASP_PORT)); + GenericContainer container = new GenericContainer<>(OWASPZAP_IMAGE); + container.setEnv(zapEnv); + container.withExposedPorts(OWASP_PORT); + container.withLogConsumer(new Slf4jLogConsumer(logger)); + container.withClasspathResourceMapping(".", DOCKER_REPORT_PATH, BindMode.READ_WRITE); + container.setCommand(String.format("zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true -config api.key=%s", ZAP_API_KEY)); + container.start(); + + int zapPort = container.getMappedPort(OWASP_PORT); + api = new ClientApi(ZAP_ADDRESS, zapPort, ZAP_API_KEY); + + // We do not want to scan Google, Facebook etc. + api.core.excludeFromProxy("^.*\\.googleapis\\.com.*$"); + api.core.excludeFromProxy("^.*\\.google\\.com.*$"); + api.core.excludeFromProxy("^.*\\.facebook\\.com.*$"); + api.core.excludeFromProxy("^.*\\.fbcdn\\.net.*$"); + api.core.excludeFromProxy("^.*\\.ip6\\.li.*$"); + + ChromeOptions chromeOptions = getChromeOptions(zapPort); + ChromeDriverService chromeDriverService = getChromeDriverService(); + chromeDriver = new ChromeDriver(chromeDriverService, chromeOptions); + } + + // This is the glue between Selenium and OWASP Zap + @NotNull + private static ChromeOptions getChromeOptions(int zapPort) { + String proxyServerUrl = String.format("%s:%d", OwaspZapTest.ZAP_ADDRESS, zapPort); + Proxy proxy = new Proxy(); + proxy.setHttpProxy(proxyServerUrl); + proxy.setSslProxy(proxyServerUrl); + + ChromeOptions chromeOptions = new ChromeOptions(); + // OWASP Zap proxy uses a self signed server certificate + chromeOptions.addArguments("--ignore-ssl-errors=yes"); + chromeOptions.addArguments("--ignore-certificate-errors"); + chromeOptions.addArguments("--lang=en-US,en"); + chromeOptions.setAcceptInsecureCerts(true); + chromeOptions.setProxy(proxy); + return chromeOptions; + } + + @NotNull + private static ChromeDriverService getChromeDriverService() { + return new ChromeDriverService.Builder() + .withLogOutput(System.err) + .build(); + } + + @AfterAll + public static void closeAll() throws InterruptedException { + String title = "ZAP Selenium"; + String template = "traditional-html"; + String description = "This is a ZAP report for Alovoa"; + String reportFilename = "zap-report.html"; + try { + ApiResponse res = api.reports.generate( + title, + template, + null, + description, + null, + null, + null, + null, + null, + reportFilename, + null, + DOCKER_REPORT_PATH, null); + logger.info(String.format("ZAP report generated here: %s", res.toString())); + } catch (ClientApiException ex) { + logger.error(String.format("closeAll report: %s", ex.getMessage())); + } + + Thread.sleep(2000); + + chromeDriver.quit(); + } + + @Test + public void alovoaSecurityAssessment() throws InterruptedException { + + // localhost does not work here, because OWASP Zap is running in a container + final String targetUrl = String.format("%s:%d", appDomain, alovoaPort); + + ChromeDriver driver = chromeDriver; + driver.get(targetUrl); + OwaspZapTest.executeSpiderScan(targetUrl); + OwaspZapTest.executeActiveScan(targetUrl); + Thread.sleep(2000); + driver.findElement(By.id("login")).click(); + Thread.sleep(2000); + driver.findElement(By.name("username")).sendKeys(EMAIL); + driver.findElement(By.name("password")).sendKeys(PASSWORD); + driver.findElement(By.id("send_login")).click(); + + Thread.sleep(2000); + if (driver.findElement(By.linkText("Search")).isDisplayed()) { + OwaspZapTest.executeSpiderScan(targetUrl); + OwaspZapTest.executeActiveScan(targetUrl); + } + } + +} diff --git a/src/test/java/com/nonononoki/alovoa/dast/README.md b/src/test/java/com/nonononoki/alovoa/dast/README.md new file mode 100644 index 00000000..e6006542 --- /dev/null +++ b/src/test/java/com/nonononoki/alovoa/dast/README.md @@ -0,0 +1,59 @@ +# Dynamic Application Security Test (DAST) + +Today it is essential to do security tests for every application. +This directory contains a solution for dynamic application security tests (DAST) with +[OWASP Zap](https://www.zaproxy.org/) (Zed Attack Proxy) provides by OWASP. This +unit test uses following components: + +* [TestContainers](https://java.testcontainers.org/) +* [Selenium](https://www.selenium.dev/) +* [Dockerized OWASP Zap](https://hub.docker.com/r/softwaresecurityproject/zap-stable) + +There is a tight interaction between Selenium an OWASP Zap, which needs some explanation to understand +what happens here. + +* Selenium is an automation platform which allows to control a web browser by Java code, in this case a unit test. +* Selenium configures browser to use a proxy server. Proxy server is OWASP Zap, which is configured to attack Alovoa on localhost. +* Now it should be obvious that Alovoa must be fully configured and operable, so it can be tested. + +# Requirements + +Local authentication **must** be enabled, set *app.local.login.enabled=true* otherwise Zap test will fail. + +Captchas must be disabled: + +``` +app.captcha.login.enabled=false +app.captcha.delete.enabled=false +app.captcha.imprint.enabled=false +app.captcha.password.enabled=false +app.captcha.register.enabled=false +``` + +app.domain must be set to a hostname wich must be: + +* resolvable from inside Docker container (OWASP Zap) +* IP adresse msut be reachable from inside Docker container (OWASP Zap) + + app.domain=http://alovoa.test.felsing.net + +# False Positives + +OWASP Zap delivers a report which shows some security flaws. That means from a network view on application. +There may be possible security flaws. Every finding must be checked carefully. + +# Alovoa HTML + +To do effective tests, Selenium needs a little bit help. That means all input fields regarding authentication needs +an id so Selenium is able to enter authn data. Other input fields are attacked by OWASP Zap automatically. + +# Running Test + +While running this unit test a Chrome or Chromium browser will appear on screen. **Do not try to interact with +that browser window**. Otherwise you will unexpected results. + +While tests application may throw many exceptions like + + ERROR c.n.a.component.ExceptionHandler - java.lang.IllegalArgumentException: Locale part "Set-cookie:" contains invalid characters + +This is ok, in a real attack such messages will appear in your log file and you may trigger a fail2ban rule.