implemented ssl stripping checks and hsts checks

This commit is contained in:
ltiongku
2024-07-18 21:10:04 +08:00
parent b966597ca6
commit 2771ac4f73
11 changed files with 107 additions and 19 deletions

View File

@@ -12,4 +12,9 @@ public class CommonConstants {
public static final String QR_CODE_TYPE_SMS = "SMS"; public static final String QR_CODE_TYPE_SMS = "SMS";
public static final String QR_CODE_TYPE_URL = "URL"; public static final String QR_CODE_TYPE_URL = "URL";
public static final String QR_CODE_TYPE_WIFI = "WIFI"; public static final String QR_CODE_TYPE_WIFI = "WIFI";
public static final String INFO_NON_SECURE_CONNECTION = "Not an HTTPS connection";
public static final String INFO_NO_HSTS_HEADER = "No HSTS Header detected";
public static final String INFO_HSTS_HEADER_PREFIX = "HSTS Header: ";
public static final String INFO_HSTS_NOT_APPLICABLE = "N/A";
} }

View File

@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Builder; import lombok.Builder;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator; import org.hibernate.annotations.UuidGenerator;
import java.util.UUID; import java.util.UUID;
@@ -13,10 +15,12 @@ import java.util.UUID;
@Table(name = "email", schema = "safeqr") @Table(name = "email", schema = "safeqr")
@Data @Data
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmailEntity { public class EmailEntity {
@Id @Id
@JsonIgnore @JsonIgnore
@GeneratedValue(generator = "UUID") @GeneratedValue(strategy = GenerationType.AUTO)
@UuidGenerator @UuidGenerator
@Column(updatable = false, nullable = false) @Column(updatable = false, nullable = false)
private UUID id; private UUID id;

View File

@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Builder; import lombok.Builder;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator; import org.hibernate.annotations.UuidGenerator;
import java.util.UUID; import java.util.UUID;
@@ -13,10 +15,12 @@ import java.util.UUID;
@Table(name = "phone", schema = "safeqr") @Table(name = "phone", schema = "safeqr")
@Data @Data
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor
public class PhoneEntity { public class PhoneEntity {
@Id @Id
@JsonIgnore @JsonIgnore
@GeneratedValue(generator = "UUID") @GeneratedValue(strategy = GenerationType.AUTO)
@UuidGenerator @UuidGenerator
@Column(updatable = false, nullable = false) @Column(updatable = false, nullable = false)
private UUID id; private UUID id;

View File

@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Builder; import lombok.Builder;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator; import org.hibernate.annotations.UuidGenerator;
import java.util.UUID; import java.util.UUID;
@@ -13,10 +15,12 @@ import java.util.UUID;
@Table(name = "sms", schema = "safeqr") @Table(name = "sms", schema = "safeqr")
@Data @Data
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor
public class SMSEntity { public class SMSEntity {
@Id @Id
@JsonIgnore @JsonIgnore
@GeneratedValue(generator = "UUID") @GeneratedValue(strategy = GenerationType.AUTO)
@UuidGenerator @UuidGenerator
@Column(updatable = false, nullable = false) @Column(updatable = false, nullable = false)
private UUID id; private UUID id;

View File

@@ -1,12 +1,16 @@
package com.safeqr.app.qrcode.entity; package com.safeqr.app.qrcode.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.NoArgsConstructor;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "scan_history", schema = "safeqr") @Table(name = "scan_history", schema = "safeqr")
public class ScanHistoryEntity { public class ScanHistoryEntity {

View File

@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Builder; import lombok.Builder;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator; import org.hibernate.annotations.UuidGenerator;
import java.util.UUID; import java.util.UUID;
@@ -13,10 +15,12 @@ import java.util.UUID;
@Table(name = "text", schema = "safeqr") @Table(name = "text", schema = "safeqr")
@Data @Data
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor
public class TextEntity { public class TextEntity {
@Id @Id
@JsonIgnore @JsonIgnore
@GeneratedValue(generator = "UUID") @GeneratedValue(strategy = GenerationType.AUTO)
@UuidGenerator @UuidGenerator
@Column(updatable = false, nullable = false) @Column(updatable = false, nullable = false)
private UUID id; private UUID id;

View File

@@ -45,6 +45,12 @@ public class URLEntity {
private int redirect = 0; private int redirect = 0;
@Column(name = "hsts_header")
private List<String> hstsHeader;
@Column(name = "ssl_stripping")
private List<Boolean> sslStripping;
@Column(name = "redirect_chain") @Column(name = "redirect_chain")
private List<String> redirectChain; private List<String> redirectChain;
} }

View File

@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Builder; import lombok.Builder;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator; import org.hibernate.annotations.UuidGenerator;
import java.util.UUID; import java.util.UUID;
@@ -13,10 +15,12 @@ import java.util.UUID;
@Table(name = "wifi", schema = "safeqr") @Table(name = "wifi", schema = "safeqr")
@Data @Data
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor
public class WifiEntity { public class WifiEntity {
@Id @Id
@JsonIgnore @JsonIgnore
@GeneratedValue(generator = "UUID") @GeneratedValue(strategy = GenerationType.AUTO)
@UuidGenerator @UuidGenerator
@Column(updatable = false, nullable = false) @Column(updatable = false, nullable = false)
private UUID id; private UUID id;

View File

@@ -35,11 +35,9 @@ public class URLModel extends QRCodeModel {
String url = scannedQRCode.getContents(); String url = scannedQRCode.getContents();
try { try {
details = urlVerificationService.breakdownURL(url); details = urlVerificationService.breakdownURL(url);
List<String> redirectChain = urlVerificationService.countAndTrackRedirects(url); urlVerificationService.countAndTrackRedirects(url, details);
// set qrCode Identifier // set qrCode Identifier
details.setQrCodeId(scannedQRCode.getId()); details.setQrCodeId(scannedQRCode.getId());
details.setRedirect(redirectChain.size() - 1);
details.setRedirectChain(redirectChain);
// Insert into URL table // Insert into URL table
urlVerificationService.insertDB(details); urlVerificationService.insertDB(details);

View File

@@ -1,7 +1,7 @@
package com.safeqr.app.qrcode.service; package com.safeqr.app.qrcode.service;
import com.safeqr.app.constants.CommonConstants; import static com.safeqr.app.constants.CommonConstants.*;
import com.safeqr.app.qrcode.dto.QRCodePayload; import com.safeqr.app.qrcode.dto.QRCodePayload;
import com.safeqr.app.qrcode.dto.response.BaseScanResponse; import com.safeqr.app.qrcode.dto.response.BaseScanResponse;
import com.safeqr.app.qrcode.entity.QRCodeEntity; import com.safeqr.app.qrcode.entity.QRCodeEntity;
@@ -22,6 +22,8 @@ import reactor.core.publisher.Mono;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service @Service
public class QRCodeTypeService { public class QRCodeTypeService {
@@ -49,6 +51,7 @@ public class QRCodeTypeService {
private List<QRCodeTypeEntity> configs; private List<QRCodeTypeEntity> configs;
private QRCodeTypeEntity defaultQRCodeTypeEntity; private QRCodeTypeEntity defaultQRCodeTypeEntity;
private Map<Long, String> tableMap;
@PostConstruct @PostConstruct
public void loadQRCodeTypes() { public void loadQRCodeTypes() {
@@ -56,9 +59,12 @@ public class QRCodeTypeService {
configs = qrCodeTypeRepository.findAll(); configs = qrCodeTypeRepository.findAll();
// Set the default QR Code Type // Set the default QR Code Type
defaultQRCodeTypeEntity = configs.stream() defaultQRCodeTypeEntity = configs.stream()
.filter(config -> config.getType().equals(CommonConstants.DEFAULT_QR_CODE_TYPE)) .filter(config -> config.getType().equals(DEFAULT_QR_CODE_TYPE))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
// Construct the tableMap with key = qrCodeTypeId, value = tableName
tableMap = configs.stream().collect(Collectors.toMap(QRCodeTypeEntity::getId, QRCodeTypeEntity::getTableName));
logger.info("Table map: {}", tableMap);
} }
public List<QRCodeTypeEntity> getAllTypes() { public List<QRCodeTypeEntity> getAllTypes() {
@@ -95,12 +101,17 @@ public class QRCodeTypeService {
return BaseScanResponse.builder().qrcode(qrCodeModel).build(); return BaseScanResponse.builder().qrcode(qrCodeModel).build();
} }
// Returns Default type as text if it does not fit into any of the category
private QRCodeTypeEntity getQRCodeType(String data) { private QRCodeTypeEntity getQRCodeType(String data) {
return configs.stream() return configs.stream()
.filter(config -> data.toLowerCase().startsWith(config.getPrefix().toLowerCase())) .filter(config -> data.toLowerCase().startsWith(config.getPrefix().toLowerCase()))
.findFirst() .findFirst()
.orElse(defaultQRCodeTypeEntity); .orElse(defaultQRCodeTypeEntity);
} }
// Returns name of table given type
public String getTableMap(Long qrTypeId) {
return tableMap.get(qrTypeId);
}
public Mono<String> detectType(QRCodePayload payload) { public Mono<String> detectType(QRCodePayload payload) {
String data = payload.getData(); String data = payload.getData();

View File

@@ -1,6 +1,6 @@
package com.safeqr.app.qrcode.service; package com.safeqr.app.qrcode.service;
import com.safeqr.app.constants.CommonConstants; import static com.safeqr.app.constants.CommonConstants.*;
import com.safeqr.app.qrcode.dto.QRCodePayload; import com.safeqr.app.qrcode.dto.QRCodePayload;
import com.safeqr.app.qrcode.dto.URLVerificationResponse; import com.safeqr.app.qrcode.dto.URLVerificationResponse;
import com.safeqr.app.qrcode.entity.URLEntity; import com.safeqr.app.qrcode.entity.URLEntity;
@@ -10,12 +10,11 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException; import java.io.IOException;
import java.net.*; import java.net.*;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service @Service
public class URLVerificationService { public class URLVerificationService {
@@ -70,15 +69,17 @@ public class URLVerificationService {
// set query params to URL query // set query params to URL query
urlObj.setQuery(queryParams.toString()); urlObj.setQuery(queryParams.toString());
// set fragment to URL ref // set fragment to URL ref
urlObj.setFragment(url.getRef()); urlObj.setFragment(Optional.ofNullable(url.getRef()).orElse(""));
return urlObj; return urlObj;
} }
public List<String> countAndTrackRedirects(String urlString) throws IOException, URISyntaxException { public void countAndTrackRedirects(String urlString, URLEntity details) throws IOException, URISyntaxException {
URI uri = new URI(urlString); URI uri = new URI(urlString);
URL url = uri.toURL(); URL url = uri.toURL();
List<String> redirectChain = new ArrayList<>(); List<String> redirectChain = new ArrayList<>();
List<String> hstsHeaderList = new ArrayList<>();
List<Boolean> sslStrippingList = new ArrayList<>();
// Add the initial URL to the chain // Add the initial URL to the chain
redirectChain.add(urlString); redirectChain.add(urlString);
@@ -86,12 +87,25 @@ public class URLVerificationService {
int redirectCount = 0; int redirectCount = 0;
do { do {
URLConnection testConnection = url.openConnection();
if (!(testConnection instanceof HttpURLConnection)) {
// Handle non-HTTP connections (like mailto:)
logger.info("Non-HTTP URL encountered: {}", url);
hstsHeaderList.add(INFO_HSTS_NOT_APPLICABLE);
sslStrippingList.add(false);
break;
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET"); connection.setRequestMethod("GET");
connection.setInstanceFollowRedirects(false); connection.setInstanceFollowRedirects(false);
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
redirected = (responseCode >= 300 && responseCode < 400); redirected = (responseCode >= 300 && responseCode < 400);
// Checks for HSTS Header
hstsHeaderList.add(detectHSTSHeader(url, connection));
// Handle redirects // Handle redirects
if (redirected) { if (redirected) {
// Location header contains the URL to redirect to // Location header contains the URL to redirect to
@@ -99,19 +113,49 @@ public class URLVerificationService {
if (newUrl == null) { if (newUrl == null) {
break; break;
} }
URI newUri = uri.resolve(newUrl);
// check for SSL stripping during redirect
sslStrippingList.add(checkRedirectForSSLStripping(uri, newUri));
// Handle relative URLs // Handle relative URLs
uri = uri.resolve(newUrl); uri = uri.resolve(newUrl);
url = uri.toURL(); url = uri.toURL();
redirectChain.add(url.toString()); redirectChain.add(url.toString());
redirectCount++; redirectCount++;
logger.info("Redirect #{}: {}",redirectCount, newUrl); logger.info("Redirect #{}: {}",redirectCount, newUrl);
} else {
// No redirect, so no SSL stripping
sslStrippingList.add(false);
} }
connection.disconnect(); connection.disconnect();
} while (redirected && redirectCount < CommonConstants.MAX_REDIRECT_COUNT); } while (redirected && redirectCount < MAX_REDIRECT_COUNT);
return redirectChain; details.setRedirect(redirectChain.size() - 1);
details.setRedirectChain(redirectChain);
details.setSslStripping(sslStrippingList);
details.setHstsHeader(hstsHeaderList);
} }
// Function to check if the redirect is from HTTPS to HTTP
private boolean checkRedirectForSSLStripping(URI originalUri, URI newUri) {
return originalUri.getScheme().equalsIgnoreCase("https") &&
newUri.getScheme().equalsIgnoreCase("http");
}
// Function to check if HSTS header is present for HTTPS connections
private String detectHSTSHeader(URL url, HttpURLConnection connection) {
if (connection instanceof HttpsURLConnection) {
String hstsHeader = connection.getHeaderField("Strict-Transport-Security");
if (hstsHeader != null && !hstsHeader.isEmpty()) {
logger.info("HSTS Header detected for {}: {}", url, hstsHeader);
return INFO_HSTS_HEADER_PREFIX + hstsHeader;
} else {
logger.warn("No HSTS Header for HTTPS connection to {}", url);
return INFO_NO_HSTS_HEADER;
}
}
return INFO_NON_SECURE_CONNECTION;
}
public URLVerificationResponse verifyURL(QRCodePayload payload) { public URLVerificationResponse verifyURL(QRCodePayload payload) {
URLVerificationResponse response = new URLVerificationResponse(); URLVerificationResponse response = new URLVerificationResponse();
try { try {