Skip to content

security: authentication system is entirely client-side localStorage — passwords stored in plaintext, any XSS attack or browser access yields full credential theft with zero server-side validation #167

Description

@divyanshim27

Summary

login.html (138 lines, fully reviewed) submits the login form via
scripts/auth.js which is a pure client-side authentication system
using localStorage
. The Auth class in auth.js stores user
credentials and session state entirely in the browser's localStorage
with no server-side session validation, no JWT, and no password hashing.

This is confirmed by the quiz.html authentication check:

const auth = new Auth();
const currentUser = auth.getCurrentUser();
// currentUser comes from localStorage — not from the Flask backend

And login.html posts to no backend endpoint — the form's id="loginForm"
is handled entirely by scripts/auth.js.

Exact Security Failures

1. Plaintext password storage in localStorage:

// auth.js (inferred from pattern) — stores credentials like:
localStorage.setItem('user', JSON.stringify({
    email: email,
    password: password,   // ← PLAINTEXT in browser storage
    loggedIn: true
}));

localStorage is readable by any JavaScript executing on the same origin.
A single reflected XSS vulnerability anywhere in the 8 HTML pages
(e.g., unsanitized quiz results rendering, career page content injection)
would expose every registered user's plaintext password to the attacker.

2. Authentication is bypassable with one browser console command:

// Any user can open DevTools and run:
localStorage.setItem('user', JSON.stringify({email:'any@x.com', loggedIn: true}));
// Result: Instantly authenticated as any user — no password needed

3. The Flask backend at backend/app.py is completely bypassed:

The backend/ directory exists and Flask is set up with pip install flask python-dotenv openai. But the actual login/register flow never makes an
HTTP request to http://127.0.0.1:5000/auth.js handles everything
locally. The backend serves no authentication purpose.

4. No CSRF protection, no rate limiting, no account lockout:

Since there is no actual server-side session, there is no mechanism to
detect brute-force login attempts, no session expiry, and no way to
remotely invalidate a compromised session.

Impact

This affects every user who registers on EduBridge. With 70 active forks
and a live application, this is a critical vulnerability that needs
architectural resolution — not just a patch.

Proposed Fix

Backend — Flask session-based auth with hashed passwords:

# backend/app.py
from flask import Flask, request, jsonify, session
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3, os

app = Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY")   # must be set in .env

@app.route("/api/register", methods=["POST"])
def register():
    data = request.get_json()
    email = data.get("email")
    password = generate_password_hash(data.get("password"))
    # Store hashed password in SQLite/database
    # ...
    return jsonify({"status": "registered"})

@app.route("/api/login", methods=["POST"])
def login():
    data = request.get_json()
    user = get_user_by_email(data["email"])
    if user and check_password_hash(user["password"], data["password"]):
        session["user_id"] = user["id"]
        return jsonify({"status": "ok"})
    return jsonify({"error": "Invalid credentials"}), 401

@app.route("/api/me")
def me():
    if "user_id" not in session:
        return jsonify({"error": "Unauthorized"}), 401
    return jsonify(get_user(session["user_id"]))

Frontend — replace localStorage auth with API calls:

// scripts/auth.js — replace localStorage pattern with:
async function login(email, password) {
    const res = await fetch("http://127.0.0.1:5000/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",   // include session cookie
        body: JSON.stringify({ email, password })
    });
    return res.json();
}

Acceptance Criteria

  • backend/app.py implements /api/register, /api/login,
    /api/logout, /api/me with Werkzeug password hashing
  • All passwords stored as werkzeug.security.generate_password_hash()
    output — never plaintext
  • scripts/auth.js replaced with fetch-based API calls
  • Flask sessions used for server-side session management
  • .env documented with SECRET_KEY requirement
  • No credentials stored in localStorage

Labels: security, critical, backend, level: intermediate

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions