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
4 changes: 4 additions & 0 deletions scanner/sast/expectedIssues.csv
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,7 @@ CWE-79,Reflected XSS,src/main/java/org/sasanlabs/service/vulnerability/xss/refle
CWE-79,Reflected XSS,src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java,73,1
CWE-611,Xxe,src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java,64,1
CWE-611,Xxe,src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java,137,1
CWE-352,Cross-Site Request Forgery,src/main/java/org/sasanlabs/service/vulnerability/csrf/CSRFVulnerability.java,85,1
CWE-352,Cross-Site Request Forgery,src/main/java/org/sasanlabs/service/vulnerability/csrf/CSRFVulnerability.java,137,1
CWE-352,Cross-Site Request Forgery,src/main/java/org/sasanlabs/service/vulnerability/csrf/CSRFVulnerability.java,194,1
CWE-352,Cross-Site Request Forgery,src/main/java/org/sasanlabs/service/vulnerability/csrf/CSRFVulnerability.java,266,1
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ public DataSourceInitializer adminDataSourceInitializer(
populator.addScript(new ClassPathResource("scripts/CryptographicFailures/db/schema.sql"));
populator.addScript(new ClassPathResource("scripts/SessionManagement/db/schema.sql"));
populator.addScript(new ClassPathResource("scripts/SessionManagement/db/data.sql"));
populator.addScript(new ClassPathResource("scripts/CSRF/db/schema.sql"));
populator.addScript(new ClassPathResource("scripts/CSRF/db/data.sql"));
populator.setSeparator(";");

DataSourceInitializer initializer = new DataSourceInitializer();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.sasanlabs.service.vulnerability.csrf;

/** Represents a CSRF lab account. */
public class CSRFAccount {

private int id;
private String username;
private String email;
private String storedPassword;

public CSRFAccount(int id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}

public CSRFAccount(int id, String username, String storedPassword, String email) {
this.id = id;
this.username = username;
this.storedPassword = storedPassword;
this.email = email;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getStoredPassword() {
return storedPassword;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package org.sasanlabs.service.vulnerability.csrf;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.sasanlabs.internal.utility.PasswordHashingUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
* Manages CSRF lab sessions and CSRF tokens.
*
* <p>Each level uses its own table, cookie and session namespace so that a state change in one
* level never affects the others.
*/
@Service
public class CSRFSessionService {

private static final String INVALID_CREDENTIALS = "Invalid credentials";
private static final String NOT_LOGGED_IN = "Please login first";
private static final String INVALID_TOKEN = "Invalid or missing CSRF token";
private static final String INVALID_SESSION = "Invalid session";

private static final long SESSION_TTL_MILLIS = 3600_000L;

private final JdbcTemplate jdbcTemplate;
private final SecureRandom secureRandom = new SecureRandom();

private final ConcurrentMap<String, CSRFLoginSession> sessions = new ConcurrentHashMap<>();

public CSRFSessionService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

/** Returns the account if credentials are valid, null otherwise. */
public CSRFAccount authenticate(int level, String username, String password) {
List<CSRFAccount> accounts =
jdbcTemplate.query(
"SELECT id, username, password, email FROM csrf_accounts_l"
+ level
+ " WHERE username=?",
new Object[] {username},
(rs, rowNum) ->
new CSRFAccount(
rs.getInt("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("email")));

return accounts.stream()
.filter(
account ->
account.getStoredPassword() != null
&& account.getStoredPassword().contains(":")
&& PasswordHashingUtils.isValidSaltedSha256(
password, account.getStoredPassword()))
.map(
account ->
new CSRFAccount(
account.getId(), account.getUsername(), account.getEmail()))
.findFirst()
.orElse(null);
}

/** Creates a new session for the account and returns the opaque session token. */
public String createSession(int level, CSRFAccount account) {
String sessionToken = randomHex(32);
evictExpiredSessions();
sessions.put(
sessionKey(level, sessionToken),
new CSRFLoginSession(account.getUsername(), account.getEmail()));
return sessionToken;
}

private void evictExpiredSessions() {
long now = System.currentTimeMillis();
sessions.entrySet()
.removeIf(entry -> now - entry.getValue().getCreatedAt() > SESSION_TTL_MILLIS);
}

/** Returns a securely generated CSRF token stored with the session. */
public String createSecureCsrfToken(int level, String sessionToken) {
String token = randomHex(32);
CSRFLoginSession session = getSession(level, sessionToken);
if (session == null) {
throw new IllegalArgumentException(INVALID_SESSION);
}
session.setCsrfToken(token);
return token;
}

/** Returns a predictably generated CSRF token (timestamp based, weak by design). */
public String createWeakCsrfToken(CSRFAccount account) {
return account.getUsername() + ":" + (System.currentTimeMillis() / 1000);
}

public CSRFLoginSession getSession(int level, String sessionToken) {
String key = sessionKey(level, sessionToken);
CSRFLoginSession session = sessions.get(key);
if (session != null) {
long now = System.currentTimeMillis();
if (now - session.getCreatedAt() > SESSION_TTL_MILLIS) {
sessions.remove(key);
return null;
}
}
return session;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Validates a weak (predictable) token of the form {@code username:epochSeconds}. The token
* remains valid for the entire session lifetime because the format itself is the weakness --
* any attacker who knows the username can forge a token with the current timestamp.
*/
public boolean isValidWeakToken(CSRFLoginSession session, String tokenHeaderValue) {
if (session == null || tokenHeaderValue == null) {
return false;
}
String[] parts = tokenHeaderValue.split(":", 2);
if (parts.length != 2 || !parts[0].equals(session.getUsername())) {
return false;
}
try {
Long.parseLong(parts[1]);
return true;
} catch (NumberFormatException e) {
return false;
}
}

/** Constant-time comparison against the token stored with the session. */
public boolean isValidSecureToken(CSRFLoginSession session, String tokenHeaderValue) {
if (session == null || session.getCsrfToken() == null || tokenHeaderValue == null) {
return false;
}
return MessageDigest.isEqual(
session.getCsrfToken().getBytes(StandardCharsets.UTF_8),
tokenHeaderValue.getBytes(StandardCharsets.UTF_8));
}

/** Updates the email for the given account id in the level's table. */
public void updateEmail(int level, int accountId, String newEmail) {
jdbcTemplate.update(
"UPDATE csrf_accounts_l" + level + " SET email=? WHERE id=?", newEmail, accountId);
}

public String emailOf(int level, String sessionToken) {
CSRFLoginSession session = getSession(level, sessionToken);
return session == null ? null : session.getEmail();
}

public CSRFAccount accountByUsername(int level, String username) {
List<CSRFAccount> accounts =
jdbcTemplate.query(
"SELECT id, username, email FROM csrf_accounts_l"
+ level
+ " WHERE username=?",
new Object[] {username},
(rs, rowNum) ->
new CSRFAccount(
rs.getInt("id"),
rs.getString("username"),
rs.getString("email")));
return accounts.isEmpty() ? null : accounts.get(0);
}

public void refreshSessionEmail(int level, String sessionToken) {
CSRFLoginSession session = getSession(level, sessionToken);
if (session != null) {
CSRFAccount account = accountByUsername(level, session.getUsername());
if (account != null) {
session.setEmail(account.getEmail());
}
}
}

private String randomHex(int numBytes) {
byte[] bytes = new byte[numBytes];
secureRandom.nextBytes(bytes);
StringBuilder sb = new StringBuilder(numBytes * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}

private String sessionKey(int level, String sessionToken) {
return "L" + level + ":" + sessionToken;
}

/** Simple in-memory session holding the logged in account and optional CSRF token. */
public static class CSRFLoginSession {

private final String username;
private volatile String email;
private volatile String csrfToken;
private final long createdAt = System.currentTimeMillis();

public CSRFLoginSession(String username, String email) {
this.username = username;
this.email = email;
}

public String getUsername() {
return username;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getCsrfToken() {
return csrfToken;
}

public void setCsrfToken(String csrfToken) {
this.csrfToken = csrfToken;
}

public long getCreatedAt() {
return createdAt;
}
}
}
Loading