Skip to content

Commit

Permalink
fix: Apply hardening mechanisms to controller endpoints (#325)
Browse files Browse the repository at this point in the history
* fix: Apply hardening mechanisms to controller endpoints

Signed-off-by: Oleg Kopysov <[email protected]>

* fix: Add more UTs to improve branch coverage

Signed-off-by: Oleg Kopysov <[email protected]>

* fix: Update imports in LPVSWebController

Signed-off-by: Oleg Kopysov <[email protected]>

---------

Signed-off-by: Oleg Kopysov <[email protected]>
  • Loading branch information
o-kopysov authored Nov 20, 2023
1 parent 55acf6d commit bfee439
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 65 deletions.
14 changes: 0 additions & 14 deletions src/main/java/com/lpvs/LicensePreValidationSystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.GetMapping;

import com.lpvs.util.LPVSExitHandler;

Expand Down Expand Up @@ -75,17 +74,4 @@ public TaskExecutor getAsyncExecutor() {
executor.setThreadNamePrefix("LPVS-ASYNC::");
return executor;
}

/**
* Handles the "/exit" endpoint to exit the application with the specified exit code.
*
* @param exitCode The exit code for the application.
*/
@GetMapping("/exit")
public static void exit(int exitCode) {
exitHandler.exit(exitCode);
if (exitCode != 0) {
System.exit(exitCode);
}
}
}
28 changes: 19 additions & 9 deletions src/main/java/com/lpvs/controller/GitHubWebhooksController.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,26 @@ public ResponseEntity<LPVSResponseWrapper> gitHubWebhooks(
throws Exception {
log.debug("New GitHub webhook request received");

// if signature is empty return 401
if (!StringUtils.hasText(signature)) {
return new ResponseEntity<>(new LPVSResponseWrapper(ERROR), HttpStatus.FORBIDDEN);
} else if (!GITHUB_SECRET.trim().isEmpty() && wrongSecret(signature, payload)) {
log.info("SECRET: " + GITHUB_SECRET);
log.info("WRONG: " + wrongSecret(signature, payload));
return new ResponseEntity<>(new LPVSResponseWrapper(ERROR), HttpStatus.FORBIDDEN);
// Validate and sanitize user inputs to prevent XSS attacks
// if signature is empty - return 401
if (!StringUtils.hasText(signature) || signature.length() > 72) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.headers(LPVSWebhookUtil.generateSecurityHeaders())
.body(new LPVSResponseWrapper(ERROR));
}
if (!GITHUB_SECRET.trim().isEmpty() && wrongSecret(signature, payload)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.headers(LPVSWebhookUtil.generateSecurityHeaders())
.body(new LPVSResponseWrapper(ERROR));
}

// if payload is empty, don't do anything
if (!StringUtils.hasText(payload)) {
log.debug("Response to empty payload sent");
return new ResponseEntity<>(new LPVSResponseWrapper(SUCCESS), HttpStatus.OK);
// Implement Content Security Policy (CSP) headers
return ResponseEntity.ok()
.headers(LPVSWebhookUtil.generateSecurityHeaders())
.body(new LPVSResponseWrapper(SUCCESS));
} else if (LPVSWebhookUtil.checkPayload(payload)) {
LPVSQueue webhookConfig = LPVSWebhookUtil.getGitHubWebhookConfig(payload);
webhookConfig.setDate(new Date());
Expand All @@ -150,8 +157,11 @@ public ResponseEntity<LPVSResponseWrapper> gitHubWebhooks(
queueService.addFirst(webhookConfig);
log.debug("Put Webhook config to the queue done");
}

log.debug("Response sent");
return new ResponseEntity<>(new LPVSResponseWrapper(SUCCESS), HttpStatus.OK);
return ResponseEntity.ok()
.headers(LPVSWebhookUtil.generateSecurityHeaders())
.body(new LPVSResponseWrapper(SUCCESS));
}

/**
Expand Down
134 changes: 101 additions & 33 deletions src/main/java/com/lpvs/controller/LPVSWebController.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,28 @@
import com.lpvs.repository.LPVSPullRequestRepository;
import com.lpvs.service.LPVSLoginCheckService;
import com.lpvs.service.LPVSStatisticsService;
import com.lpvs.util.LPVSWebhookUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.HtmlUtils;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* Controller class for handling web-related requests in LPVS.
Expand Down Expand Up @@ -115,34 +119,53 @@ class WebApiEndpoints {
* Retrieves personal information settings for the authenticated user.
*
* @param authentication The authentication object.
* @return LPVSMember object representing personal information settings.
* @return ResponseEntity containing LPVSMember representing personal information settings.
* The response includes security headers for enhanced security.
*/
@GetMapping("/user/info")
@ResponseBody
public LPVSMember personalInfoSettings(Authentication authentication) {
public ResponseEntity<LPVSMember> personalInfoSettings(Authentication authentication) {
lpvsLoginCheckService.loginVerification(authentication);
return lpvsLoginCheckService.getMemberFromMemberMap(authentication);
HttpHeaders headers = LPVSWebhookUtil.generateSecurityHeaders();
LPVSMember member = lpvsLoginCheckService.getMemberFromMemberMap(authentication);
sanitizeUserInputs(member);
return ResponseEntity.ok().headers(headers).body(member);
}

/**
* Retrieves login details for the authenticated user.
*
* @param authentication The authentication object.
* @return LPVSLoginMember object representing login details.
* @return ResponseEntity containing LPVSLoginMember representing login details.
* The response includes security headers for enhanced security.
*/
@GetMapping("/user/login")
@ResponseBody
public LPVSLoginMember loginMember(Authentication authentication) {
public ResponseEntity<LPVSLoginMember> loginMember(Authentication authentication) {
lpvsLoginCheckService.loginVerification(authentication);

// Include security headers in the response
HttpHeaders headers = LPVSWebhookUtil.generateSecurityHeaders();

Map<String, Object> oauthLoginMemberMap =
lpvsLoginCheckService.getOauthLoginMemberMap(authentication);
boolean isLoggedIn = oauthLoginMemberMap == null || oauthLoginMemberMap.isEmpty();

LPVSLoginMember loginMember;

if (!isLoggedIn) {
LPVSMember findMember =
lpvsLoginCheckService.getMemberFromMemberMap(authentication);
return new LPVSLoginMember(!isLoggedIn, findMember);

// Validate and sanitize user inputs to prevent XSS attacks
sanitizeUserInputs(findMember);

loginMember = new LPVSLoginMember(!isLoggedIn, findMember);
} else {
return new LPVSLoginMember(!isLoggedIn, null);
loginMember = new LPVSLoginMember(!isLoggedIn, null);
}

return ResponseEntity.ok().headers(headers).body(loginMember);
}

/**
Expand All @@ -151,20 +174,28 @@ public LPVSLoginMember loginMember(Authentication authentication) {
* @param map Map containing user settings.
* @param authentication The authentication object.
* @return ResponseEntity with LPVSMember representing the updated user.
* The response includes security headers for enhanced security.
*/
@PostMapping("/user/update")
public ResponseEntity<LPVSMember> postSettingTest(
@RequestBody Map<String, String> map, Authentication authentication) {
lpvsLoginCheckService.loginVerification(authentication);

// Include security headers in the response
HttpHeaders headers = LPVSWebhookUtil.generateSecurityHeaders();

LPVSMember findMember = lpvsLoginCheckService.getMemberFromMemberMap(authentication);
try {
findMember.setNickname(map.get("nickname"));
findMember.setOrganization(map.get("organization"));

// Validate and sanitize user inputs to prevent XSS attacks
sanitizeUserInputs(findMember);
memberRepository.saveAndFlush(findMember);
} catch (DataIntegrityViolationException e) {
throw new IllegalArgumentException("DuplicatedKeyException");
}
return ResponseEntity.ok().body(findMember);
return ResponseEntity.ok().headers(headers).body(findMember);
}

/**
Expand All @@ -174,17 +205,20 @@ public ResponseEntity<LPVSMember> postSettingTest(
* @param name The name of the user or organization.
* @param pageable The pageable object for pagination.
* @param authentication The authentication object.
* @return HistoryEntity containing a list of LPVSHistory items and total count.
* @return ResponseEntity<HistoryEntity> containing a list of LPVSHistory items and total count.
* The response includes security headers for enhanced security.
*/
@ResponseBody
@GetMapping("/history/{type}/{name}")
public HistoryEntity newHistoryPageByUser(
public ResponseEntity<HistoryEntity> newHistoryPageByUser(
@PathVariable("type") String type,
@PathVariable("name") String name,
@PageableDefault(size = 5, sort = "date", direction = Sort.Direction.DESC)
Pageable pageable,
Authentication authentication) {

HttpHeaders headers = LPVSWebhookUtil.generateSecurityHeaders();

HistoryPageEntity historyPageEntity =
lpvsLoginCheckService.pathCheck(type, name, pageable, authentication);
Page<LPVSPullRequest> prPage = historyPageEntity.getPrPage();
Expand All @@ -199,24 +233,27 @@ public HistoryEntity newHistoryPageByUser(
new Timestamp(pr.getDate().getTime()).toLocalDateTime();
String formattingDateTime = lpvsLoginCheckService.dateTimeFormatting(localDateTime);

// Validate and sanitize user inputs to prevent XSS attacks
sanitizeUserInputs(pr);

Boolean hasIssue = detectedLicenseRepository.existsIssue(pr);

lpvsHistories.add(
new LPVSHistory(
formattingDateTime,
pr.getRepositoryName(),
HtmlUtils.htmlEscape(pr.getRepositoryName()),
pr.getId(),
pr.getPullRequestUrl(),
pr.getStatus(),
pr.getSender(),
HtmlUtils.htmlEscape(pr.getPullRequestUrl()),
HtmlUtils.htmlEscape(pr.getStatus()),
HtmlUtils.htmlEscape(pr.getSender()),
pullNumberTemp[pullNumberTemp.length - 2]
+ "/"
+ pullNumberTemp[pullNumberTemp.length - 1],
hasIssue));
}

HistoryEntity historyEntity = new HistoryEntity(lpvsHistories, count);
return historyEntity;
return ResponseEntity.ok().headers(headers).body(historyEntity);
}

/**
Expand All @@ -225,20 +262,26 @@ public HistoryEntity newHistoryPageByUser(
* @param prId The pull request ID.
* @param pageable The pageable object for pagination.
* @param authentication The authentication object.
* @return LPVSResult containing result details for the specified pull request.
* @return ResponseEntity<LPVSResult> containing result details for the specified pull request.
* The response includes security headers for enhanced security.
*/
@ResponseBody
@GetMapping("/result/{prId}")
public LPVSResult resultPage(
public ResponseEntity<LPVSResult> resultPage(
@PathVariable("prId") Long prId,
@PageableDefault(size = 5, sort = "id", direction = Sort.Direction.ASC)
Pageable pageable,
Authentication authentication) {

lpvsLoginCheckService.loginVerification(authentication);
// LPVSMember findMember = lpvsLoginCheckService.getMemberFromMemberMap(authentication);
HttpHeaders headers = LPVSWebhookUtil.generateSecurityHeaders();

Optional<LPVSPullRequest> prOpt = lpvsPullRequestRepository.findById(prId);
if (!prOpt.isPresent()) {
return ResponseEntity.notFound().headers(headers).build();
}
LPVSPullRequest pr = prOpt.get();

LPVSPullRequest pr = lpvsPullRequestRepository.findById(prId).get();
List<LPVSLicense> distinctByLicense =
detectedLicenseRepository.findDistinctByLicense(pr);
List<String> detectedLicenses = new ArrayList<>();
Expand All @@ -256,8 +299,8 @@ public LPVSResult resultPage(
new LPVSResultInfo(
pr.getId(),
pr.getDate(),
pr.getRepositoryName(),
pr.getStatus(),
HtmlUtils.htmlEscape(pr.getRepositoryName()),
HtmlUtils.htmlEscape(pr.getStatus()),
detectedLicenses);

Page<LPVSDetectedLicense> dlPage =
Expand All @@ -279,8 +322,8 @@ public LPVSResult resultPage(
lpvsResultFileList.add(
new LPVSResultFile(
dl.getId(),
dl.getFilePath(),
dl.getComponentFileUrl(),
HtmlUtils.htmlEscape(dl.getFilePath()),
HtmlUtils.htmlEscape(dl.getComponentFileUrl()),
dl.getLines(),
dl.getMatch(),
status,
Expand All @@ -306,7 +349,7 @@ public LPVSResult resultPage(
+ '/'
+ tempPullNumber[tempPullNumber.length - 1],
hasIssue);
return lpvsResult;
return ResponseEntity.ok().headers(headers).body(lpvsResult);
}

/**
Expand All @@ -315,29 +358,54 @@ public LPVSResult resultPage(
* @param type The type of the dashboard (e.g., user, organization).
* @param name The name of the user or organization.
* @param authentication The authentication object.
* @return Dashboard entity containing statistics and insights.
* @return ResponseEntity<Dashboard> containing statistics and insights.
* The response includes security headers for enhanced security.
*/
@ResponseBody
@GetMapping("dashboard/{type}/{name}")
public Dashboard dashboardPage(
public ResponseEntity<Dashboard> dashboardPage(
@PathVariable("type") String type,
@PathVariable("name") String name,
Authentication authentication) {

HttpHeaders headers = LPVSWebhookUtil.generateSecurityHeaders();
Dashboard dashboardEntity =
lpvsStatisticsService.getDashboardEntity(type, name, authentication);
lpvsStatisticsService.getDashboardEntity(
type, HtmlUtils.htmlEscape(name), authentication);

return dashboardEntity;
return ResponseEntity.ok().headers(headers).body(dashboardEntity);
}
}

/**
* Redirects to the default error page.
* Validate and sanitize user inputs to prevent XSS attacks.
*
* @return String representing the path to the error page.
* @param member The LPVSMember object containing user information.
*/
@GetMapping("/error")
public String redirect() {
return "index.html";
public static void sanitizeUserInputs(LPVSMember member) {
if (member != null) {
// Sanitize user inputs using Spring's HtmlUtils
if (member.getNickname() != null)
member.setNickname(HtmlUtils.htmlEscape(member.getNickname()));
if (member.getOrganization() != null)
member.setOrganization(HtmlUtils.htmlEscape(member.getOrganization()));
}
}

/**
* Validate and sanitize user inputs to prevent XSS attacks.
*
* @param pr The LPVSPullRequest object containing user information.
*/
public static void sanitizeUserInputs(LPVSPullRequest pr) {
if (pr != null) {
// Sanitize user inputs using Spring's HtmlUtils
if (pr.getRepositoryName() != null)
pr.setRepositoryName(HtmlUtils.htmlEscape(pr.getRepositoryName()));
if (pr.getPullRequestUrl() != null)
pr.setPullRequestUrl(HtmlUtils.htmlEscape(pr.getPullRequestUrl()));
if (pr.getStatus() != null) pr.setStatus(HtmlUtils.htmlEscape(pr.getStatus()));
if (pr.getSender() != null) pr.setSender(HtmlUtils.htmlEscape(pr.getSender()));
}
}
}
Loading

0 comments on commit bfee439

Please sign in to comment.