Authentication is the front door of every application, and OWASP A07 documents how often that door is left unlocked. When I started digging into authentication failures, I realised they go far beyond weak passwords, they encompass hardcoded credentials compiled into binaries, brute-force attacks with no rate limiting, password hashes that can be reversed in seconds, and reset flows that hand tokens directly to attackers. These patterns show up in production regularly, sometimes in the same application. This post covers three CWEs across Python, Java, Go, and Rust: CWE-798 (Use of Hard-Coded Credentials), CWE-287 (Improper Authentication), and CWE-307 (Improper Restriction of Excessive Authentication Attempts).

Why Authentication Keeps Failing

Authentication code is deceptively simple. Check a username and password, issue a token, verify the token on subsequent requests. The logic is straightforward, which is exactly why developers underestimate it. The failures come from the same places every time:

  • Hardcoded credentials for “internal” use. Service accounts, bootstrap keys, debug tokens, all embedded in source code and compiled into binaries. They never rotate, they survive in version control forever, and they provide permanent backdoor access. I’ve come across these in repos that had been public on GitHub for months.
  • Weak password hashing. MD5 and SHA-256 are valid hash functions, but they’re catastrophically wrong for password storage. They’re fast, unsalted, and reversible with commodity hardware. MD5 password hashing still shows up in production code more often than you’d think.
  • Missing rate limiting. A login endpoint without throttling is an open invitation for brute-force attacks. The login logic can be perfect, but without rate limiting, it’s just a matter of time.
  • Broken reset flows. Password reset tokens returned in API responses instead of delivered via email. The email channel is the authentication factor, bypassing it defeats the entire mechanism.

The Easy-to-Spot Version

Python: Hardcoded Service API Key

SERVICE_API_KEY = "ak_prod_9f8e7d6c5b4a3210"
INTERNAL_GATEWAY_TOKEN = "gw-internal-2024-prod"

USERS = {
    1: {"id": 1, "username": "admin", "password_hash": md5_hash("admin123"),
        "role": "admin", "email": "admin@acmecorp.io"},
    2: {"id": 2, "username": "service", "password_hash": md5_hash(SERVICE_API_KEY),
        "role": "service", "email": "service@acmecorp.io"},
}

Two hardcoded credentials at the top of the file. SERVICE_API_KEY is used as the password for a privileged “service” account. INTERNAL_GATEWAY_TOKEN is accepted as a valid authentication token in the verification endpoint. Anyone with repository access has permanent privileged access to the system. This pattern typically starts as something “temporary”, but temporary credentials in source code have a way of becoming permanent.

Java: Static Final Credential Constants

private static final String SYSTEM_API_KEY = "sys_key_prod_x7y8z9w0";
private static final String MAINTENANCE_TOKEN = "maint-override-2024";

Same pattern in Java. static final strings are compiled into the class file and trivially extractable from the JAR with javap or any decompiler. The SYSTEM_API_KEY grants system-level access through the login endpoint, and MAINTENANCE_TOKEN bypasses token validation entirely. The key insight here: if it’s in the binary, it’s not a secret.

Go: Package-Level Credential Constants

const (
    internalAPIKey  = "iak_prod_7f3e2d1c0b9a8"
    debugAccessCode = "debug-access-2024-prod"
)

Go constants are embedded in the binary. The internalAPIKey serves as both a login password for the “internal” account and an API key for the debug sessions endpoint. The debugAccessCode is accepted as a valid token in the verification endpoint, creating a permanent backdoor.

Rust: Module-Level Credential Constants

const ADMIN_BOOTSTRAP_KEY: &str = "abk_prod_2024_f8e7d6c5";
const INTER_SERVICE_TOKEN: &str = "ist-mesh-auth-prod-2024";

Rust constants are compiled into the binary. The bootstrap key grants “superadmin” access through the login endpoint. The inter-service token bypasses normal session validation in the token verification endpoint.

All four languages share the same fundamental problem: credentials that should be loaded from environment variables or a secrets manager are instead hardcoded in source code. SAST tools flag these with high confidence by matching credential-like variable names assigned to string literals. This is one of the easiest findings to detect automatically, and one of the most common to find anyway.

The Hard-to-Spot Version

MD5 Password Hashing (All Four Languages)

The easy version of weak hashing is using MD5, but the way it’s used across languages makes it harder to spot than you’d expect.

Python:

import hashlib

def md5_hash(text):
    return hashlib.md5(text.encode()).hexdigest()

USERS = {
    1: {"id": 1, "username": "admin", "password_hash": md5_hash("admin123"),
        "role": "admin"},
    2: {"id": 2, "username": "jdoe", "password_hash": md5_hash("john2024!"),
        "role": "user"},
}

Java:

private static String md5Hash(String input) {
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

Go:

func md5Hash(text string) string {
    h := md5.Sum([]byte(text))
    return fmt.Sprintf("%x", h)
}

Rust:

fn compute_md5(input: &str) -> String {
    let digest = md5::compute(input.as_bytes());
    format!("{:x}", digest)
}

In every language, the hash function works correctly, it produces a valid MD5 hash. The problem is that MD5 is catastrophically wrong for password storage. It’s fast (billions of hashes per second on a GPU), unsalted (identical passwords produce identical hashes), and has known collision attacks. An attacker with access to the hashes can reverse them in seconds using rainbow tables or Hashcat.

A reviewer who sees hashlib.md5 or MessageDigest.getInstance("MD5") should flag it immediately. But in practice, the hash function is often tucked away in a utility module, and the reviewer sees md5_hash("admin123") without clicking through to the implementation. I missed this myself once when the hashing was buried in a shared utils package, it taught me to always trace the hash function back to its implementation.

Hardcoded Token Backdoors in Verification Endpoints

This pattern is more subtle than the top-level credential constants because it’s buried inside a function that also does legitimate work.

Python:

@app.route("/api/verify", methods=["POST"])
def verify_token():
    data = request.get_json()
    token = data.get("token", "") if data else ""

    if token in SESSIONS:
        session = SESSIONS[token]
        return jsonify({"valid": True, "user_id": session["user_id"],
                        "role": session["role"]})

    if token == INTERNAL_GATEWAY_TOKEN:
        return jsonify({"valid": True, "user_id": 0, "role": "gateway"})

    return jsonify({"valid": False}), 401

The function first checks the session store, legitimate. Then it checks against a hardcoded constant, backdoor. A reviewer scanning the function might see the session check and move on. The hardcoded comparison is a few lines below, easy to miss in a longer function. Backdoors like this are often added “for testing” and never removed.

Go:

func handleVerifyToken(c *gin.Context) {
    var body struct {
        Token string `json:"token"`
    }
    c.ShouldBindJSON(&body)

    mu.RLock()
    sess := sessions[body.Token]
    mu.RUnlock()

    if sess != nil {
        c.JSON(200, gin.H{"valid": true, "userId": sess.UserID, "role": sess.Role})
        return
    }

    if body.Token == debugAccessCode {
        c.JSON(200, gin.H{"valid": true, "userId": 0, "role": "debug"})
        return
    }

    c.JSON(401, gin.H{"valid": false})
}

Same pattern. The legitimate session lookup returns early if found. The hardcoded bypass is the fallback path. Any downstream service that calls this verification endpoint will trust the attacker as a debug user. What makes this dangerous is that it looks like a reasonable fallback, you have to recognise the constant name to realise it’s a backdoor.

No Rate Limiting on Login (The Absence Vulnerability)

This is the hardest authentication failure to detect because there’s nothing wrong with the code that exists, the vulnerability is in what’s missing. This one is particularly interesting to think about because developers often push back with “but the login logic is correct.”

Python:

@app.route("/api/login", methods=["POST"])
def login():
    data = request.get_json()
    username = data.get("username", "")
    password = data.get("password", "")

    user = next((u for u in USERS.values() if u["username"] == username), None)
    if not user:
        return jsonify({"error": "Invalid credentials"}), 401

    if user["password_hash"] != md5_hash(password):
        return jsonify({"error": "Invalid credentials"}), 401

    token = secrets.token_hex(32)
    SESSIONS[token] = {"user_id": user["id"], "role": user["role"]}
    return jsonify({"token": token, "role": user["role"]})

The login logic is correct: look up the user, verify the password hash, issue a token. But there’s no rate limiting, no account lockout, no progressive delay. An attacker can send thousands of requests per second, trying passwords from a leaked credential database. With MD5 hashing (which is fast), the server processes each attempt instantly.

Java:

@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> body) {
    String username = body.getOrDefault("username", "");
    String password = body.getOrDefault("password", "");

    Map<String, Object> user = null;
    for (Map<String, Object> u : USERS.values()) {
        if (username.equals(u.get("username"))) {
            user = u;
            break;
        }
    }

    if (user == null || !md5Hash(password).equals(user.get("passwordHash"))) {
        return ResponseEntity.status(401).body(Map.of("error", "Invalid credentials"));
    }

    String token = UUID.randomUUID().toString();
    SESSIONS.put(token, Map.of("userId", user.get("id"), "role", user.get("role")));
    return ResponseEntity.ok(Map.of("token", token, "role", user.get("role")));
}

SAST tools analyse code that exists, not code that’s absent. They can’t flag “this login endpoint should have rate limiting” because that’s an architectural requirement, not a code pattern. This is why CWE-307 is rated Likely_Missed across all languages, and it’s why checking for rate limiting needs to be a separate, deliberate step in any auth review.

Password Reset Token in API Response

Rust:

async fn forgot_password(
    body: web::Json<ForgotRequest>,
    state: web::Data<AppState>,
) -> HttpResponse {
    let users = state.users.lock().unwrap();
    let user = users.values().find(|u| u.email == body.email);

    if let Some(user) = user {
        let token = format!("{:x}", md5::compute(format!("{}{}", user.email, 
            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs())));
        
        let mut tokens = state.reset_tokens.lock().unwrap();
        tokens.insert(token.clone(), user.id);

        return HttpResponse::Ok().json(serde_json::json!({
            "message": "Reset email sent",
            "token": token
        }));
    }

    HttpResponse::NotFound().json(serde_json::json!({"error": "Email not found"}))
}

The reset token is returned directly in the response. The email channel, which is supposed to prove the requester owns the account, is bypassed entirely. An attacker sends a forgot-password request for any email and immediately gets the token to reset the password. The message says “Reset email sent” but the token is right there in the JSON. The lie is right in the response body, it’s almost too on-the-nose.

Go:

func handleForgotPassword(c *gin.Context) {
    var body struct {
        Email string `json:"email"`
    }
    c.ShouldBindJSON(&body)

    mu.RLock()
    var targetUser *User
    for _, u := range users {
        if u.Email == body.Email {
            targetUser = u
            break
        }
    }
    mu.RUnlock()

    if targetUser == nil {
        c.JSON(404, gin.H{"error": "Email not found"})
        return
    }

    token := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s%d",
        targetUser.Email, time.Now().UnixNano()))))

    mu.Lock()
    resetTokens[token] = targetUser.ID
    mu.Unlock()

    c.JSON(200, gin.H{"message": "Reset email sent", "token": token})
}

Both examples also use MD5 to generate the reset token from the email and current time, making the token predictable even if it weren’t returned in the response. It’s a double failure: the token is both leaked and guessable.

Detection Strategies

SAST Tool Coverage

Pattern Bandit (Python) SpotBugs (Java) gosec (Go) clippy (Rust)
Hardcoded credentials Limited
MD5 password hashing Limited
Missing rate limiting No No No No
Token in response No No No No
Hardcoded token backdoor Partial Partial Partial No

The pattern is clear: SAST tools catch the obvious cases (hardcoded credentials, MD5 usage) but miss the architectural issues (rate limiting) and business logic flaws (token exposure, backdoor comparisons). Relying on SAST alone for auth reviews isn’t enough, you need a human looking at the flow end-to-end.

Manual Review Strategy

Here’s the checklist I’ve developed for reviewing authentication code:

  1. Search for credential patterns: Grep for API_KEY, TOKEN, SECRET, PASSWORD in constant/variable assignments. Verify they load from environment variables, not string literals.
  2. Audit password hashing: Find every password hash function. Verify it uses bcrypt, scrypt, or Argon2, not MD5, SHA-1, or SHA-256.
  3. Check login endpoints for rate limiting: Every authentication endpoint should have throttling. If there’s no rate limiting middleware, it’s a CWE-307 finding.
  4. Trace password reset flows: Verify reset tokens are delivered via email only, never returned in API responses.
  5. Look for backdoor comparisons: In token verification functions, check for comparisons against constants. Any if token == SOME_CONSTANT in an auth context is a backdoor.
  6. Check debug endpoints: Search for routes containing debug, internal, admin. Verify they don’t expose session tokens or accept hardcoded credentials.

Remediation

Load Credentials from Environment Variables

# Python
import os
SERVICE_API_KEY = os.environ["SERVICE_API_KEY"]
// Java
private static final String SYSTEM_API_KEY = System.getenv("SYSTEM_API_KEY");
// Go
var internalAPIKey = os.Getenv("INTERNAL_API_KEY")
// Rust
let admin_key = std::env::var("ADMIN_BOOTSTRAP_KEY")
    .expect("ADMIN_BOOTSTRAP_KEY must be set");

Use Proper Password Hashing

# Python
import bcrypt

def hash_password(password):
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

def verify_password(password, stored_hash):
    return bcrypt.checkpw(password.encode(), stored_hash.encode())
// Java
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hash = encoder.encode(password);
boolean matches = encoder.matches(password, hash);
// Go
import "golang.org/x/crypto/bcrypt"

hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
err := bcrypt.CompareHashAndPassword(hash, []byte(password))

Add Rate Limiting

# Python (Flask-Limiter)
from flask_limiter import Limiter
limiter = Limiter(app, default_limits=["100 per hour"])

@app.route("/api/login", methods=["POST"])
@limiter.limit("5 per minute")
def login():
    # ... existing logic

Never Return Reset Tokens in Responses

@app.route("/api/password-reset", methods=["POST"])
def request_password_reset():
    # ... find user, generate token
    send_email(user["email"], reset_token)
    # Always return the same message regardless of whether the email exists
    return jsonify({"message": "If the email exists, a reset link has been sent"})

Here’s what clicked for me while researching this: authentication is a system, not a function. Getting the login logic right is necessary but not sufficient. You also need proper credential management (no hardcoding), proper password storage (bcrypt, not MD5), proper throttling (rate limiting on every auth endpoint), and proper reset flows (tokens via email only). Miss any one of these, and the authentication system is broken. Teams can nail the login logic and completely miss everything else, and that’s how accounts get compromised.