OculusCyber Logo

OculusCyber

Home

Browse Topics


SQL Injection A bad code and Good Fix Example

By Admin

November 9, 2025


Bad example — classic SQL Injection (trash)

// BAD: vulnerable to SQL Injection
// Example: /login?username=admin'--&password=whatever
public class AuthServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String user = req.getParameter("username");
        String pass = req.getParameter("password");

        try (Connection conn = DataSource.getConnection();
             Statement stmt = conn.createStatement()) {

            // DANGEROUS: concatenating untrusted input directly into SQL
            String sql = "SELECT id, username FROM users WHERE username = '" + user +
                         "' AND password = '" + pass + "'";
            ResultSet rs = stmt.executeQuery(sql);

            if (rs.next()) {
                resp.getWriter().println("Welcome " + rs.getString("username"));
            } else {
                resp.getWriter().println("Invalid credentials");
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Why it's broken

  • User-controlled values are concatenated into SQL → attacker injects '; DROP TABLE users; -- or bypasses auth with admin' --.
  • No parameterization, no input control, no least-privilege DB user assumption.
  • Credentials may be stored in cleartext (implied), no hashing.

✅ Good fix — JDBC with PreparedStatement (parameterized)

// GOOD: parameterized query with hashed password + try-with-resources
public class AuthServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String user = req.getParameter("username");
        String pass = req.getParameter("password");

        // 1) Validate minimal shape (length, allowed chars) - whitelist when possible
        if (user == null || pass == null || user.length() > 100) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // 2) Hash the incoming password using a secure storage/compare routine (e.g., bcrypt)
        String hashedInput = PasswordUtils.hashForCompare(pass); // uses bcrypt/argon2 in production

        String sql = "SELECT id, username FROM users WHERE username = ? AND password_hash = ?";
        try (Connection conn = DataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql)) {

            ps.setString(1, user);
            ps.setString(2, hashedInput); // never store or compare plain-text passwords
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    resp.getWriter().println("Welcome " + rs.getString("username"));
                } else {
                    resp.getWriter().println("Invalid credentials");
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Why this is better

  • PreparedStatement binds values — prevents injection.
  • Input minimal validation reduces risk and DoS vectors.
  • Passwords compared as hashes (use bcrypt/argon2 with per-user salt).
  • Use least-privilege DB account (only SELECT for auth) and connection pool.

✅ Good alternative — JPA / Hibernate (safe parameter binding)

// GOOD: Using JPA with named parameters (no string concat)
@Stateless
public class AuthService {
    @PersistenceContext
    private EntityManager em;

    public User authenticate(String username, String plainPassword) {
        if (username == null) return null;

        TypedQuery<User> q = em.createQuery(
            "SELECT u FROM User u WHERE u.username = :username", User.class);
        q.setParameter("username", username);
        List<User> users = q.getResultList();
        if (users.isEmpty()) return null;

        User u = users.get(0);
        if (PasswordUtils.verify(plainPassword, u.getPasswordHash())) {
            return u;
        }
        return null;
    }
}

Why this is good

  • JPA parameter binding avoids manual SQL concatenation.
  • Move business logic & crypto outside DB string builds.
  • ORM reduces injection risk when used correctly (avoid createQuery() with concatenated strings).

Additional hardening — short checklist (non-negotiable)

  1. Always use parameterized queries (PreparedStatement, Named parameters, ORM binding).
  2. Never build SQL with user input (including column/table names) — if you must accept identifiers, whitelist them server-side.
  3. Hash & salt passwords with bcrypt/argon2; never store plain text.
  4. Least-privilege DB user: app DB user should have only required rights (SELECT/INSERT/UPDATE only as needed).
  5. Use stored procedures only with parameters (not dynamic SQL inside SPs).
  6. Escape properly only as last resort — prefer parameterization.
  7. Logging & monitoring for abnormal queries and repeated auth failures.
  8. Test: run dynamic scanning (DAST) and SAST to catch unsafe patterns.
  9. Prepare for errors: don't leak SQL or stack traces to users. Return generic errors.