Use of Client-Side Authentication

Description

Use of Client-Side Authentication occurs when an application performs authentication or credential verification primarily or exclusively on the client side. This includes storing authentication state in client-accessible locations (localStorage, cookies without httpOnly, URL parameters), performing password comparison in client-side code, or allowing client-determined authentication status to control access. Attackers can manipulate client-side authentication to gain unauthorized access.

Risk

Client-side authentication provides no real security. Attackers can set authentication flags in browser storage, modify JavaScript to skip authentication checks, or directly call protected APIs. Credentials stored client-side may be exposed through XSS. Authentication tokens without server validation can be forged. This is one of the most severe security weaknesses as it essentially means the application has no authentication at all.

Solution

Always perform authentication on the server. Use server-generated, cryptographically secure session tokens. Store sessions server-side or use signed tokens (JWT) that the server validates. Never trust client-reported authentication status. Implement proper session management with httpOnly, Secure cookies. Validate authentication on every protected request. Consider multi-factor authentication for sensitive operations.

Common Consequences

ImpactDetails
AuthenticationScope: Complete Bypass

Attackers can authenticate as any user.
AuthorizationScope: Privilege Escalation

Access controls based on client data fail.
ConfidentialityScope: Data Exposure

All protected data becomes accessible.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Authentication state in localStorage
function login(username, password) {
    // Pretend to validate on server
    fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password })
    });

    // Store auth state client-side - attacker can modify!
    localStorage.setItem('isLoggedIn', 'true');
    localStorage.setItem('username', username);
    localStorage.setItem('role', 'user');

    // Attacker in console: localStorage.setItem('role', 'admin')
}

function checkAuth() {
    // Security entirely client-side!
    if (localStorage.getItem('isLoggedIn') === 'true') {
        return true;
    }
    return false;
}

function requireAdmin() {
    // Attacker: localStorage.setItem('role', 'admin')
    if (localStorage.getItem('role') === 'admin') {
        showAdminPanel();
    }
}

// VULNERABLE: Password comparison in JavaScript
async function authenticateVulnerable(username, password) {
    // Fetch user data including password!
    const response = await fetch(`/api/users/${username}`);
    const user = await response.json();

    // Compare password client-side - exposed in network/memory
    if (user.password === password) {
        sessionStorage.setItem('authenticated', 'true');
        return true;
    }
    return false;
}

// VULNERABLE: URL-based authentication
function checkUrlAuth() {
    const params = new URLSearchParams(window.location.search);

    // Attacker: ?authenticated=true&admin=true
    if (params.get('authenticated') === 'true') {
        if (params.get('admin') === 'true') {
            showAdminDashboard();
        } else {
            showUserDashboard();
        }
    }
}

// VULNERABLE: Cookie without httpOnly set by JS
function setAuthCookie(token) {
    // Can be stolen via XSS!
    document.cookie = `auth_token=${token}; path=/`;
}
<!-- VULNERABLE: Hidden form field for authentication -->
<form action="/dashboard" method="POST">
    <input type="hidden" name="authenticated" value="true" />
    <input type="hidden" name="userId" value="12345" />
    <input type="hidden" name="role" value="admin" />
    <!-- Attacker modifies these with dev tools -->
    <button type="submit">Access Dashboard</button>
</form>

<!-- VULNERABLE: JavaScript auth gate -->
<script>
function protectedPage() {
    // This check is meaningless!
    if (!window.isAuthenticated) {
        window.location.href = '/login';
        return;  // Attacker: window.isAuthenticated = true
    }

    loadSensitiveData();
}
</script>
// VULNERABLE: Trusting client authentication headers
@WebServlet("/api/data")
public class VulnerableApiServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        // Trust client-provided authentication!
        String authenticated = request.getHeader("X-Authenticated");
        String userId = request.getHeader("X-User-Id");

        // Attacker adds headers to any request
        if ("true".equals(authenticated)) {
            serveData(response, userId);
        } else {
            response.sendError(401);
        }
    }
}

// VULNERABLE: Client-side session ID validation
@WebServlet("/dashboard")
public class VulnerableDashboardServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        // Get session ID from client parameter
        String sessionId = request.getParameter("sessionId");

        // No server-side validation of session!
        if (sessionId != null && !sessionId.isEmpty()) {
            // Attacker: ?sessionId=anything
            serveDashboard(response);
        }
    }
}
# VULNERABLE: Flask with client-trusted auth
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/sensitive')
def get_sensitive_vulnerable():
    # Trust client-provided user ID!
    user_id = request.headers.get('X-User-ID')
    is_admin = request.headers.get('X-Is-Admin') == 'true'

    # Attacker sets any headers they want
    if is_admin:
        return jsonify(get_all_data())
    elif user_id:
        return jsonify(get_user_data(user_id))

    return jsonify({'error': 'Not authenticated'}), 401

Fixed Code

// SAFE: Server-side authentication with secure tokens
async function login(username, password) {
    const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
        credentials: 'include'  // Include cookies
    });

    if (response.ok) {
        // Server sets httpOnly, Secure cookie
        // Client doesn't handle token directly
        return true;
    }
    return false;
}

// SAFE: Verify authentication with server on every check
async function checkAuth() {
    try {
        const response = await fetch('/api/verify', {
            credentials: 'include'
        });

        if (response.ok) {
            const user = await response.json();
            return user;  // Server-verified user data
        }
    } catch (e) {
        console.error('Auth check failed');
    }
    return null;
}

// SAFE: Server determines authorization
async function loadDashboard() {
    const response = await fetch('/api/dashboard', {
        credentials: 'include'
    });

    if (response.status === 401) {
        window.location.href = '/login';
        return;
    }

    if (response.status === 403) {
        showAccessDenied();
        return;
    }

    // Server only returns data user is authorized to see
    const data = await response.json();
    displayDashboard(data);
}

// SAFE: Admin check is server-side
async function checkAdminAccess() {
    const response = await fetch('/api/admin/verify', {
        credentials: 'include'
    });

    return response.ok;
}
// SAFE: Server-side session management
@WebServlet("/api/login")
public class SafeLoginServlet extends HttpServlet {

    @Inject
    private AuthService authService;

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // Validate credentials server-side
        User user = authService.authenticate(username, password);

        if (user != null) {
            // Create server-side session
            HttpSession session = request.getSession(true);
            session.setAttribute("userId", user.getId());
            session.setAttribute("role", user.getRole());

            // Session ID automatically in httpOnly cookie
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().write("{\"success\": true}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
}

// SAFE: Verify session on every request
@WebServlet("/api/data")
public class SafeDataServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        // Get session from container (validates cookie)
        HttpSession session = request.getSession(false);

        if (session == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Get user from SERVER-SIDE session
        Long userId = (Long) session.getAttribute("userId");
        if (userId == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Serve data for authenticated user
        serveUserData(response, userId);
    }
}

// SAFE: JWT validation on server
@WebFilter("/api/*")
public class JwtAuthFilter implements Filter {

    @Inject
    private JwtService jwtService;

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        String token = authHeader.substring(7);

        try {
            // SERVER validates JWT signature and claims
            Claims claims = jwtService.validateToken(token);

            // Attach user info to request
            request.setAttribute("userId", claims.getSubject());
            request.setAttribute("role", claims.get("role"));

            chain.doFilter(request, response);

        } catch (JwtException e) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
}
# SAFE: Flask with server-side authentication
from flask import Flask, request, session, jsonify
from functools import wraps
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# Configure secure session cookie
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Lax'
)

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        # Check SERVER-SIDE session
        if 'user_id' not in session:
            return jsonify({'error': 'Unauthorized'}), 401

        # Load user from database
        user = User.query.get(session['user_id'])
        if not user:
            session.clear()
            return jsonify({'error': 'Invalid session'}), 401

        # Attach to request context
        request.current_user = user
        return f(*args, **kwargs)
    return decorated

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

    # SERVER validates credentials
    user = User.query.filter_by(username=username).first()

    if user and user.check_password(password):
        # Create SERVER-SIDE session
        session['user_id'] = user.id
        session['role'] = user.role

        return jsonify({'success': True})

    return jsonify({'error': 'Invalid credentials'}), 401

@app.route('/api/sensitive')
@require_auth
def get_sensitive_safe():
    # user_id comes from SERVER session
    return jsonify(get_user_data(request.current_user.id))

@app.route('/api/admin/data')
@require_auth
def get_admin_data():
    # Check role from SERVER session
    if request.current_user.role != 'admin':
        return jsonify({'error': 'Forbidden'}), 403

    return jsonify(get_all_data())

@app.route('/api/verify')
@require_auth
def verify_auth():
    # Endpoint to verify authentication status
    return jsonify({
        'authenticated': True,
        'user_id': request.current_user.id,
        'role': request.current_user.role
    })

Exploited in the Wild

Single-Page Application Bypasses

SPAs with client-side auth checks were bypassed by manipulating localStorage/sessionStorage.

Mobile App Authentication Bypass

Mobile apps storing auth state locally were exploited by modifying app data.

Hidden Admin Panels

Admin interfaces protected only by JavaScript were discovered and accessed.


Tools to test/exploit

  • Browser developer tools — modify storage, cookies.

  • Burp Suite — intercept and modify requests.

  • Postman — call APIs without client.

  • Mobile app decompilers — analyze client-side code.


CVE Examples

  • CVEs from authentication bypass in web applications.

  • Mobile app vulnerabilities from client-side auth.


References

  1. MITRE. "CWE-603: Use of Client-Side Authentication." https://cwe.mitre.org/data/definitions/603.html

  2. OWASP. "Authentication Cheat Sheet." https://cheatsheetseries.owasp.org/