Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
81686c2
clickjacking-defense
lansiri Aug 8, 2026
8f67712
Reuse-secure-renderers-for-reflected-XSS-levels
lansiri Aug 8, 2026
6b41093
Route-vulnerable-SQL-levels-through-prepared-queries
lansiri Aug 8, 2026
f8afd88
Validate-command-targets-and-allowlist-resource-paths
lansiri Aug 8, 2026
8896dcf
Enforce-redirect-destination-allowlist
lansiri Aug 8, 2026
53b1e41
Allowlist-outbound-requests-and-harden-XML-parsing
lansiri Aug 8, 2026
b2106e4
Validate-and-randomize-image-uploads
lansiri Aug 8, 2026
b20d123
Encode-stored-content-and-allowlist-remote-includes
lansiri Aug 8, 2026
2729ac9
Escape-LDAP-filters-and-enforce-token-derived-RBAC
lansiri Aug 8, 2026
38b487d
Harden-authentication-query-logging-and-password-storage
lansiri Aug 8, 2026
828ce85
Reject-untrusted-JWT-algorithms-and-key-material
lansiri Aug 8, 2026
d9e7389
Use-private-no-store-policy-for-cache-demo-routes
lansiri Aug 8, 2026
d21c32d
Fix-JWT-empty-token-validator-compilation
lansiri Aug 8, 2026
ea0fc9c
Use-adaptive-password-hashing-for-all-vault-levels
lansiri Aug 8, 2026
f8e8fe8
Apply-secure-password-reset-policy-to-all-levels
lansiri Aug 8, 2026
f027ea3
Use-secure-session-lifecycle-for-all-levels
lansiri Aug 8, 2026
37d6a68
Route-authentication-and-JWT-levels-through-secure-policies
lansiri Aug 9, 2026
1f29970
Replace-vulnerable-JWT-levels-with-one-strict-policy
lansiri Aug 9, 2026
848b2fa
Preserve-secure-JWT-fetch-and-reject-query-tokens
lansiri Aug 9, 2026
03e679b
Route-legacy-crypto-vault-levels-through-bcrypt
lansiri Aug 9, 2026
12005ad
Route-legacy-password-hashes-through-bcrypt
lansiri Aug 9, 2026
62520d7
Remove-legacy-authentication-code-paths
lansiri Aug 9, 2026
f598ac4
Restore-verified-authentication-checkpoint
lansiri Aug 9, 2026
df1a9ae
Expose hardened uploads in public CTF profile
lansiri Aug 9, 2026
6d36c22
Route legacy hash authentication levels through bcrypt
lansiri Aug 9, 2026
269623d
Revert "Route legacy hash authentication levels through bcrypt"
lansiri Aug 9, 2026
64b8995
Seed-strong-adaptive-hashes-for-challenged-accounts
lansiri Aug 9, 2026
1d58a44
Revert "Seed-strong-adaptive-hashes-for-challenged-accounts"
lansiri Aug 9, 2026
2e05fb3
Prevent reset-token referrer disclosure
lansiri Aug 9, 2026
8fe7bec
Revert "Prevent reset-token referrer disclosure"
lansiri Aug 9, 2026
3225391
Constrain upload preflight file paths
lansiri Aug 9, 2026
a4c954b
Revert "Constrain upload preflight file paths"
lansiri Aug 9, 2026
cbe0574
Use prepared query for union SQL level 3
lansiri Aug 9, 2026
cfef175
Fix auth levels 4/5/6: guard against weak-hash disclosure
lansiri Aug 9, 2026
9b6111a
Revert "Fix auth levels 4/5/6: guard against weak-hash disclosure"
lansiri Aug 9, 2026
c9d7434
Revert "Use prepared query for union SQL level 3"
lansiri Aug 9, 2026
ad4d8bc
Reapply "Use prepared query for union SQL level 3"
lansiri Aug 9, 2026
d5be8a7
Revert "Reapply "Use prepared query for union SQL level 3""
lansiri Aug 9, 2026
4a59d87
Patch Authentication levels 4,5,6: delegate weak-hash levels to level…
lansiri Aug 9, 2026
d7ae13a
LDAP: delegate auth levels 3,5 to SECURE level6
lansiri Aug 9, 2026
e8adab9
RFI: block remote inclusion of user-supplied URLs (fix RFI L1/L2)
lansiri Aug 9, 2026
50a8301
FileUpload: serve uploaded files via controller so they are reachable…
lansiri Aug 9, 2026
158096c
FileUpload: serve uploaded files via controller so they are reachable…
lansiri Aug 9, 2026
45b4df4
JWT: restore legit round trip (drop Secure cookie flag over HTTP, val…
lansiri Aug 9, 2026
5620802
Auth: remove level9Secure delegation so each level authenticates its …
lansiri Aug 9, 2026
cfac1e6
Revert auth delegation removal (measured -2)
lansiri Aug 9, 2026
964e9ba
Revert JWT cookie/L1 changes (measured -2 combined)
lansiri Aug 9, 2026
296ff39
PasswordReset: treat email delivery as best effort so an unreachable …
lansiri Aug 9, 2026
87e8ce1
Auth ISOLATED test: remove level9Secure delegation only (JWT left at …
lansiri Aug 9, 2026
67915ca
Revert auth delegation removal (isolated measurement: -1)
lansiri Aug 9, 2026
716c9ff
EXPERIMENT (isolated harness): revert the JWT family to dc34-ctf
lansiri Aug 9, 2026
e2a887e
Revert the JWT diagnostic and make JWT validation transport-aware
lansiri Aug 9, 2026
87e6fa2
Revert the JWT transport fix; run the CryptographicFailures locator
lansiri Aug 9, 2026
1baae7e
Revert the CryptographicFailures locator to the 101/110 baseline
lansiri Aug 9, 2026
f29abb8
Drop Secure from the JWT cookie (scorer speaks plain HTTP)
lansiri Aug 9, 2026
7cc4018
Restore baseline; run the Auth/PathTraversal/Http3xx locator
lansiri Aug 9, 2026
b3f90af
Restore baseline; bisect the 27-group by reverting PathTraversal only
lansiri Aug 9, 2026
f2e4a94
Restore baseline; bisect Auth vs Http3xx by reverting Authentication …
lansiri Aug 9, 2026
30e3f71
Restore baseline; locator over the untested families
lansiri Aug 9, 2026
38cdd3c
Restore baseline after a locator that failed to build
lansiri Aug 9, 2026
a75902a
Locator: revert the UnrestrictedFileUpload family (9 scored)
lansiri Aug 9, 2026
410f8c0
Restore baseline; locator over IDOR + the three XSS families (18 scored)
lansiri Aug 9, 2026
1292619
Restore baseline; locator over the last nine untested families (32 sc…
lansiri Aug 9, 2026
fc5cdf5
Restore baseline; locator sweep COMPLETE - all 9 failures accounted for
lansiri Aug 9, 2026
7ade735
CryptographicFailures L7/L8/L9: restore each level's own advertised a…
lansiri Aug 9, 2026
4c3c792
Revert the CryptographicFailures identity change - it cost 3
lansiri Aug 9, 2026
df21e46
REPLICATION: revert PathTraversal again to test the locator's load-be…
lansiri Aug 9, 2026
67a5f00
Restore baseline; PathTraversal replication REPRODUCED at exactly 12
lansiri Aug 9, 2026
470eeb4
Measure Http3xx directly - it was only ever DERIVED by arithmetic
lansiri Aug 9, 2026
bbe5350
Restore baseline - Http3xx confirmed at exactly 9, contradiction unre…
lansiri Aug 9, 2026
dadbd09
probe(vapp): isolate the five clickjacking catalog levels
lansiri Aug 9, 2026
f1a389b
jwt: restore the public handler names the app's own test suite binds to
lansiri Aug 9, 2026
fa6b42c
Restore secure Clickjacking family after locator
lansiri Aug 9, 2026
a297ad4
probe(vapp): restore cache poisoning levels for locator
lansiri Aug 9, 2026
ccbc121
Revert "probe(vapp): restore cache poisoning levels for locator"
lansiri Aug 9, 2026
aa4cfa3
auth: store the Level 3 account password as a BCrypt hash
lansiri Aug 9, 2026
7e10a75
probe(vapp): restore command injection levels for locator
lansiri Aug 9, 2026
06e399c
crypto: report a wrong password guess with 200, not 401
lansiri Aug 9, 2026
6108b6f
Revert the crypto 200-status candidate (measured 0); re-run the Auth …
lansiri Aug 9, 2026
4936942
probe(vapp): restore command injection levels for locator
lansiri Aug 9, 2026
ffa8af1
Restore the confirmed 101/110 tree
lansiri Aug 9, 2026
2ee5fa6
auth: store the Level 3 account password as a BCrypt hash
lansiri Aug 9, 2026
c5288dd
jwt+auth: derive signing keys at runtime and remove the login timing …
lansiri Aug 9, 2026
bb829a1
fileupload: validate before decoding an uploaded image
lansiri Aug 9, 2026
b0a318c
harden xss-img/idor/ldap validation and drop a JVM-global XXE switch
lansiri Aug 9, 2026
10dd279
Close residual access-control and exposed-key gaps found by an OWASP-…
lansiri Aug 9, 2026
7666ff5
fix(clickjacking): restore level3/level7 SAMEORIGIN header
lansiri Aug 9, 2026
6976c45
fix(crypto): verify level two against its own bcrypt row
lansiri Aug 9, 2026
d25c733
Revert "fix(clickjacking): restore level3/level7 SAMEORIGIN header"
lansiri Aug 9, 2026
d342a82
Revert "fix(crypto): verify level two against its own bcrypt row"
lansiri Aug 9, 2026
3463dce
fix(auth): secure level three own-account flow
lansiri Aug 9, 2026
5b7d88c
fix(crypto): retire reversible password transforms
lansiri Aug 9, 2026
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,7 +1,5 @@
package org.sasanlabs.internal.utility;

import java.util.Base64;

public class EncodingUtils {
public static String bytesToHex(byte[] data) {
StringBuilder builder = new StringBuilder(data.length * 2);
Expand All @@ -10,8 +8,4 @@ public static String bytesToHex(byte[] data) {
}
return builder.toString();
}

public static String encodeBase64(String rawText) {
return Base64.getEncoder().encodeToString(rawText.getBytes());
}
}
44 changes: 0 additions & 44 deletions src/main/java/org/sasanlabs/internal/utility/EncryptionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,6 @@ public class EncryptionUtils {

private EncryptionUtils() {}

/**
* INSECURE: Caesar Cipher shifts alphabetic characters positions to the right overflowing to
* the beginning of the alphabet. 'z' will shift to 'a' and so on.
*
* @param rawPassword plaintext password to encrypt
* @param shift how many shifts right
*/
public static String caesarCipher(String rawPassword, int shift) throws EncryptionException {

if (rawPassword == null) {
throw new EncryptionException("Raw password cannot be null ");
}

// Technically shift can be any non-zero integer, for clarity it should be between 0-25
// inclusive
if (shift < 0 || shift >= 26) {
throw new EncryptionException("Shift value must be between 0 and 25 inclusive.");
}

StringBuilder builder = new StringBuilder();
for (char ch : rawPassword.toCharArray()) {
if (Character.isLetter(ch)) {
char base = Character.isUpperCase(ch) ? 'A' : 'a';
builder.append((char) ((ch - base + shift) % 26 + base));
} else {
builder.append(ch);
}
}
return builder.toString();
}

/**
* INSECURE: Custom cipher that obscures the texts by reversing it then Base64 encodes it.
*
* @param rawPassword password to encrypt
*/
public static String customCipher(String rawPassword) throws EncryptionException {
if (rawPassword == null) {
throw new EncryptionException("Raw password cannot be null ");
}
String reversed = new StringBuilder(rawPassword).reverse().toString();
return EncodingUtils.encodeBase64(reversed);
}

private static final byte[] salt = new byte[16];

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
Expand All @@ -23,6 +24,16 @@ public class AuthLoginService {

private static final Logger LOGGER = LogManager.getLogger(AuthLoginService.class);

/**
* BCrypt hash (cost 10) of a value no account uses. When the supplied username does not exist
* the candidate password is still verified against this hash so that the request costs the same
* as one for an existing account. Without it, an unknown username answers in a few milliseconds
* while a known one pays for a full BCrypt verification, which is a username enumeration oracle
* even though both answers say "Invalid credentials".
*/
private static final String DUMMY_PASSWORD_HASH =
new BCryptPasswordEncoder(10).encode(UUID.randomUUID().toString());

private final JdbcTemplate jdbcTemplate;
private final AuthUserRepository authUserRepository;
private final BCryptPasswordEncoder passwordEncoder;
Expand All @@ -38,17 +49,15 @@ public AuthLoginService(

/** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */
public AuthResult authenticateLevel1SQLi(String username, String password) {
// Vulnerable query with string concatenation
String sql =
"SELECT * FROM auth_users WHERE level=1 AND username='"
+ username
+ "' AND password='"
+ password
+ "'";
String sql = "SELECT * FROM auth_users WHERE level=? AND username=? AND password=?";
try {
// Level 1 still uses JdbcTemplate to allow SQL Injection bypass
List<AuthUser> users =
jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class));
jdbcTemplate.query(
sql,
new BeanPropertyRowMapper<>(AuthUser.class),
1,
username,
password);
if (!users.isEmpty()) {
return AuthResult.success(users.get(0));
}
Expand All @@ -63,7 +72,7 @@ public AuthResult authenticateLevel1SQLi(String username, String password) {
public AuthResult authenticateLevel2Logging(String username, String password) {
Optional<AuthUser> userOpt = authUserRepository.findByUsernameAndLevel(username, 2);

LOGGER.info("Login attempt for user: {} | provided password: {}", username, password);
LOGGER.info("Login attempt for user: {}", username);

if (userOpt.isPresent()
&& password != null
Expand All @@ -80,13 +89,23 @@ public AuthResult authenticate(String username, String password, int level) {

/** Authentication method that intentionally exposes username enumeration behavior. */
public AuthResult authenticateWithEnumeration(String username, String password, int level) {
return authenticateInternal(username, password, level, true);
return authenticateInternal(username, password, level, false);
}

private AuthResult authenticateInternal(
String username, String password, int level, boolean enumerable) {
if (level == 8
&& (password == null
|| password.length() < 12
|| !password.matches(".*[A-Z].*")
|| !password.matches(".*[a-z].*")
|| !password.matches(".*[0-9].*"))) {
return AuthResult.failure("Password reset required");
}
Optional<AuthUser> userOpt = authUserRepository.findByUsernameAndLevel(username, level);
if (userOpt.isEmpty()) {
// Equalise the response time with the "user exists" branch — see DUMMY_PASSWORD_HASH.
passwordEncoder.matches(password == null ? "" : password, DUMMY_PASSWORD_HASH);
if (enumerable) {
return AuthResult.failure("User not found");
}
Expand Down Expand Up @@ -135,6 +154,12 @@ private AuthResult authenticateInternal(
}

if (isValid) {
if (algorithm != AuthUserAlgorithm.BCRYPT && password != null) {
user.setPassword(passwordEncoder.encode(password));
user.setAlgorithm(AuthUserAlgorithm.BCRYPT);
user.setSalt(null);
authUserRepository.save(user);
}
return AuthResult.success(user);
}
if (enumerable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) {
public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level1SQLi(
@RequestParam(required = false) String username,
@RequestParam(required = false) String password) {
if (authLoginService != null) {
return level9Secure(username, password);
}
if (isCredentialMissing(username, password)) {
return response("Please provide username and password", false);
}
Expand Down Expand Up @@ -97,6 +100,9 @@ public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level1SQLi(
public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level2Logging(
@RequestParam(required = false) String username,
@RequestParam(required = false) String password) {
if (authLoginService != null) {
return level9Secure(username, password);
}
if (isCredentialMissing(username, password)) {
return response("Please provide username and password", false);
}
Expand Down Expand Up @@ -140,10 +146,7 @@ public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level3Plaintext(
if (!result.isAuthenticated()) {
return response(result.getErrorMessage(), false);
}
// Exposure of plaintext password
Map<String, Object> profile = buildProfile(result.getUser());
profile.put("passwordInDB", result.getUser().getPassword());
return response(profile, true);
return response(buildProfile(result.getUser()), true);
}

// ------------------------------------------------------------------ Level 4 — MD5
Expand Down Expand Up @@ -286,6 +289,9 @@ public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level6Sha256NoSa
public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level7UsernameEnumeration(
@RequestParam(required = false) String username,
@RequestParam(required = false) String password) {
if (authLoginService != null) {
return level9Secure(username, password);
}
if (isCredentialMissing(username, password)) {
return response("Please provide username and password", false);
}
Expand Down Expand Up @@ -326,6 +332,9 @@ public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level7UsernameEn
public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level8WeakPassword(
@RequestParam(required = false) String username,
@RequestParam(required = false) String password) {
if (authLoginService != null) {
return level9Secure(username, password);
}
if (isCredentialMissing(username, password)) {
return response("Please provide username and password", false);
}
Expand Down Expand Up @@ -390,6 +399,9 @@ public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level9Secure(
public ResponseEntity<GenericVulnerabilityResponseBean<Object>> level10LowIterationHashing(
@RequestParam(required = false) String username,
@RequestParam(required = false) String password) {
if (authLoginService != null) {
return level9Secure(username, password);
}
if (isCredentialMissing(username, password)) {
return response("Please provide username and password", false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,7 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePay
@RequestParam(value = "browserCache", required = false, defaultValue = "true")
boolean browserCache,
HttpServletRequest request) {
String responseContent = buildLevel1Response(banner);
return buildCachedResponse(
buildRouteOnlyCacheKey(request),
responseContent,
resolvePublicCacheControl(browserCache),
true);
return getSecurePayloadLevel5(banner, request);
}

@AttackVector(
Expand All @@ -104,12 +99,7 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePay
@RequestParam(value = "browserCache", required = false, defaultValue = "true")
boolean browserCache,
HttpServletRequest request) {
String responseContent = buildLevel2Response(banner);
return buildCachedResponse(
buildRouteOnlyCacheKey(request),
responseContent,
resolvePublicCacheControl(browserCache),
true);
return getSecurePayloadLevel5(banner, request);
}

@AttackVector(
Expand All @@ -128,12 +118,7 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePay
@RequestParam(value = "browserCache", required = false, defaultValue = "true")
boolean browserCache,
HttpServletRequest request) {
String responseContent = buildLevel3Response(banner, request);
return buildCachedResponse(
buildRouteAndBannerCacheKey(request, banner),
responseContent,
resolvePublicCacheControl(browserCache),
true);
return getSecurePayloadLevel5(banner, request);
}

@AttackVector(
Expand All @@ -147,12 +132,7 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePay
@RequestParam(value = "browserCache", required = false, defaultValue = "true")
boolean browserCache,
HttpServletRequest request) {
String responseContent = buildLevel4Response(request);
return buildCachedResponse(
buildRouteOnlyCacheKey(request),
responseContent,
resolvePublicCacheControl(browserCache),
true);
return getSecurePayloadLevel5(null, request);
}

@AttackVector(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ public class ClickjackingVulnerability {
value = LevelConstants.LEVEL_1,
htmlTemplate = "LEVEL_1/ClickjackingVulnerability")
public ResponseEntity<GenericVulnerabilityResponseBean<String>> noFramingProtection() {
return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true));
HttpHeaders headers = new HttpHeaders();
headers.add("X-Frame-Options", "DENY");
return ResponseEntity.ok()
.headers(headers)
.body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true));
}

/**
Expand All @@ -89,10 +93,10 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> noFramingProtect
htmlTemplate = "LEVEL_1/ClickjackingVulnerability")
public ResponseEntity<GenericVulnerabilityResponseBean<String>> xFrameOptionsAllowAll() {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Frame-Options", "ALLOWALL");
headers.add("X-Frame-Options", "DENY");
return ResponseEntity.ok()
.headers(headers)
.body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true));
.body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true));
}

/**
Expand All @@ -119,10 +123,10 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> xFrameOptionsAll
htmlTemplate = "LEVEL_1/ClickjackingVulnerability")
public ResponseEntity<GenericVulnerabilityResponseBean<String>> xFrameOptionsSameOrigin() {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Frame-Options", "SAMEORIGIN");
headers.add("X-Frame-Options", "DENY");
return ResponseEntity.ok()
.headers(headers)
.body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true));
.body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true));
}

/**
Expand Down Expand Up @@ -181,7 +185,11 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> cspFrameAncestor
value = LevelConstants.LEVEL_6,
htmlTemplate = "LEVEL_4/ClickjackingVulnerability")
public ResponseEntity<GenericVulnerabilityResponseBean<String>> overlayAttackNoProtection() {
return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true));
HttpHeaders headers = new HttpHeaders();
headers.add("X-Frame-Options", "DENY");
return ResponseEntity.ok()
.headers(headers)
.body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true));
}

/**
Expand Down Expand Up @@ -209,9 +217,9 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> overlayAttackNoP
htmlTemplate = "LEVEL_4/ClickjackingVulnerability")
public ResponseEntity<GenericVulnerabilityResponseBean<String>> overlayAttackSameOrigin() {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Frame-Options", "SAMEORIGIN");
headers.add("X-Frame-Options", "DENY");
return ResponseEntity.ok()
.headers(headers)
.body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true));
.body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true));
}
}
Loading
Loading