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
| Impact | Details |
|---|---|
| Authentication | Scope: Complete Bypass Attackers can authenticate as any user. |
| Authorization | Scope: Privilege Escalation Access controls based on client data fail. |
| Confidentiality | Scope: 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
-
MITRE. "CWE-603: Use of Client-Side Authentication." https://cwe.mitre.org/data/definitions/603.html
-
OWASP. "Authentication Cheat Sheet." https://cheatsheetseries.owasp.org/