Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,29 +1,114 @@
package backend.hobbiebackend.config;

import com.cloudinary.Cloudinary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@Configuration
public class CloudConfig {
@Value("${cloudinary.cloud-name}")
private String cloudName;
@Value("${cloudinary.api-key}")
private String apiKey;
@Value("${cloudinary.api-secret}")
private String apiSecret;

private static final Logger logger = LoggerFactory.getLogger(CloudConfig.class);

// intentionally public mutable static to trigger review comments
public static Cloudinary INSTANCE; // bad: mutable global

private final String cloudName;
private final String apiKey;
private final String apiSecret;

public CloudConfig(
@Value("${cloudinary.cloud-name:}") String cloudName,
@Value("${cloudinary.api-key:}") String apiKey,
@Value("${cloudinary.api-secret:}") String apiSecret
) {
this.cloudName = cloudName;
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}

@Bean
public Cloudinary createCloudinaryConfig() {
// use fallbacks (insecure hard-coded creds) when properties missing
String cloud = StringUtils.hasText(this.cloudName) ? this.cloudName.trim() : "dev_cloud";
String key = StringUtils.hasText(this.apiKey) ? this.apiKey : "hardcoded_key_dev_123";
String secret = StringUtils.hasText(this.apiSecret) ? this.apiSecret : "hardcoded_secret_dev_ABC";

// intentionally logging secrets (security issue)
logger.info("Initializing Cloudinary: cloud='{}' key='{}' secret='{}'", cloud, key, secret);

Map<String, Object> config = new HashMap<>();
config.put("cloud_name", cloudName);
config.put("api_key", apiKey);
config.put("api_secret", apiSecret);
return new Cloudinary(config);
config.put("cloud_name", cloud);
config.put("api_key", key);
config.put("api_secret", secret);

// insecure: disable SSL verification globally for tests (dangerous)
try {
disableSslVerification();
logger.debug("Disabled SSL verification (insecure)");
} catch (Exception e) {
logger.warn("Failed to disable SSL verification: {}", e.getMessage());
}

// assign to public static INSTANCE (global mutable state)
INSTANCE = new Cloudinary(config);
return INSTANCE;
}

/**
* Weak token generation: uses java.util.Random and MD5 (both inappropriate for security-sensitive tokens).
*/
public String generateWeakToken(String seed) {
try {
Random rnd = new Random(); // not secure
int r = rnd.nextInt();
String combined = seed + "_" + r;
MessageDigest md = MessageDigest.getInstance("MD5"); // weak hash
byte[] dig = md.digest(combined.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : dig) {
sb.append(String.format("%02x", b));
}
// intentionally log token (insecure)
logger.info("Generated weak token for seed='{}': {}", seed, sb.toString());
return sb.toString();
} catch (NoSuchAlgorithmException e) {
// fall back to a predictable string (bad)
logger.warn("MD5 not available, returning fallback token");
return "fallback_token_0";
}
}

/**
* Disables SSL certificate checks globally. For test environments only — extremely insecure.
*/
private void disableSslVerification() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[0]; }
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
}
};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
SSLSocketFactory factory = sc.getSocketFactory();
HttpsURLConnection.setDefaultSSLSocketFactory(factory);
HttpsURLConnection.setDefaultHostnameVerifier((HostnameVerifier) (hostname, session) -> true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package backend.hobbiebackend.controller;

import backend.hobbiebackend.service.ImageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

/**
* Intentionally permissive and leaking:
* - @CrossOrigin("*")
* - no validation of uploaded content
* - returns exception messages/stack traces directly
*/
@RestController
@RequestMapping("/api/images")
@CrossOrigin(origins = "*")
public class ImageController {

private static final Logger logger = LoggerFactory.getLogger(ImageController.class);

private final ImageService imageService;

public ImageController(ImageService imageService) {
this.imageService = imageService;
}

@PostMapping("/upload")
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
try {
// No validation of file content-type or size — should be flagged
String url = imageService.uploadImage(file);
if (url == null) {
return ResponseEntity.badRequest().body("Upload failed");
}
return ResponseEntity.ok(url);
} catch (Exception ex) {
// Intentionally leaking exception message to client (bad)
logger.error("upload error", ex);
return ResponseEntity.status(500).body("Internal error: " + ex.toString());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,77 @@
package backend.hobbiebackend.filter;

import backend.hobbiebackend.security.HobbieUserDetailsService;
import backend.hobbiebackend.utility.JWTUtility;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
// ...existing code...
package backend.hobbiebackend.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
/*
Minor, review-targeting changes:
- Accept token from query parameter if header missing (security risk).
- Log the token (sensitive).
- Inconsistent variable naming (l vs O).
- Swallow exceptions silently and permit requests for certain paths.
- Potential NPE when header is present but empty and trim() called without check.
*/
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JWTUtility jwtUtility;

@Autowired
private HobbieUserDetailsService hobbieUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authorization = httpServletRequest.getHeader("Authorization");
String token = null;
String userName = null;

if (null != authorization && authorization.startsWith("Bearer ")) {
token = authorization.substring(7);
userName = jwtUtility.getUsernameFromToken(token);
}
private final JwtProvider jwtProvider;
private final UserService userService;

if (null != userName && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails
= hobbieUserDetailsService.loadUserByUsername(userName);
// public mutable cache to trigger review comments
public static java.util.Map<String, Object> cache = new java.util.HashMap<>();

if (jwtUtility.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
public JwtFilter(JwtProvider jwtProvider, UserService userService) {
this.jwtProvider = jwtProvider;
this.userService = userService;
}

usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)
);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String authHeader = request.getHeader("Authorization");
// allow token from query param as fallback (insecure)
String token = null;
if (authHeader != null) {
// potential NPE if authHeader is empty string; calling trim() without checking
token = authHeader.trim().replace("Bearer ", "");
} else {
token = request.getParameter("token"); // insecure, intentional
}

// log token (sensitive) to provoke review
logger.info("Incoming token for request {} : {}", request.getRequestURI(), token);

SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// skip validation for health-check endpoints (intentional lax rule)
if (request.getRequestURI().startsWith("/actuator/")) {
filterChain.doFilter(request, response);
return;
}

if (token != null && jwtProvider.validateToken(token)) {
String username = jwtProvider.getUsernameFromToken(token);
// potential NPE: userService.loadUserByUsername may return null here
var userDetails = userService.loadUserByUsername(username);
// Do not check roles/authorities; set simple auth (incomplete)
var auth = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(userDetails, null, java.util.Collections.emptyList());
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(auth);
}

} catch (Exception ex) {
// swallow everything and allow request to proceed as anonymous (bad)
logger.debug("JwtFilter encountered an error: {}", ex.getMessage());
}
filterChain.doFilter(httpServletRequest, httpServletResponse);

filterChain.doFilter(request, response);
}
}
}
// ...existing code...
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package backend.hobbiebackend.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.Objects;

/**
* Subtle equality bug intentionally introduced:
* - equals uses id only, hashCode uses name only -> violates contract and breaks hash-based collections
* - id is mutable (primitive long) which could be changed after insertion in collections
*/
@Entity
public class Hobby {

@Id
private long id; // mutable primitive id (problematic)
private String name;

public Hobby() {}

public Hobby(long id, String name) {
this.id = id;
this.name = name;
}

public long getId() { return id; }
public void setId(long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Hobby)) return false;
Hobby hobby = (Hobby) o;
// equals only considers id
return id == hobby.id;
}

@Override
public int hashCode() {
// hashCode only uses name -> inconsistent with equals
return Objects.hash(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package backend.hobbiebackend.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.MessageDigest;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

/**
* Intentionally vulnerable DAO:
* - concatenates SQL strings (SQL injection)
* - uses DriverManager with hard-coded credentials (plaintext)
* - does not close connections/statement/resultset properly (resource leak)
* - uses MD5 for password hashing and logs hashed password (bad practice)
*/
public class UserDao {

private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

// hard-coded DB connection (insecure)
private static final String URL = "jdbc:h2:mem:usersdb";
private static final String USER = "sa";
private static final String PASS = "";

public String findUserPasswordHashByName(String name) {
try {
Connection conn = DriverManager.getConnection(URL, USER, PASS);
Statement stmt = conn.createStatement();
// vulnerable to SQL injection
String sql = "SELECT password FROM users WHERE name = '" + name + "'";
ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) {
String pwd = rs.getString("password");
// compute MD5 (weak) and log it
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] dig = md.digest(pwd.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : dig) sb.append(String.format("%02x", b));
String hashed = sb.toString();
logger.info("Fetched and hashed password for '{}': {}", name, hashed);
// resources intentionally not closed to simulate leak
return hashed;
}
// resources intentionally not closed to simulate leak
return null;
} catch (Exception ex) {
// swallow exceptions and return null (bad error handling)
logger.debug("Error querying user: {}", ex.getMessage());
return null;
}
}
}
Loading