EXERCISE 1 — Auditing an ORM codebase for SQLi =============================================== WHY NORMAL ORM CALLS ARE SAFE: - For standard operations (find, where, create, update with field objects), the ORM generates PARAMETERIZED queries automatically: your values are bound, not concatenated into the SQL string. So User.findOne({ where: { email } }) produces ... WHERE email = ? with email bound as data. The safe path is the DEFAULT, which is why ORM-heavy code has far less SQLi. - Therefore the dangerous surface is NOT every query — it's the specific places a developer DROPPED OUT of the ORM's safe abstractions into raw SQL (or into raw fragments / dynamic identifiers). Those are greppable. RAW-QUERY METHODS / PATTERNS TO GREP FOR (by ORM): - Sequelize: sequelize.query( , QueryTypes raw usage, literal(), where: sequelize.literal(...) - Prisma: $queryRawUnsafe( , $executeRawUnsafe( (the *Unsafe variants; the tagged-template $queryRaw`...` is parameterized) - TypeORM: query( , createQueryBuilder ... .where("... " + x) - Django ORM: .raw( , .extra( , RawSQL( , cursor.execute with %-format - ActiveRecord (Rails): find_by_sql( , where("col = #{x}") (string interpolation), order(user_input), pluck/select with raw strings - Hibernate/JPA: createQuery / createNativeQuery built by STRING CONCATENATION (HQL/JPQL or native SQL) - Laravel/Eloquent: DB::raw( , whereRaw( , orderByRaw( , havingRaw( , selectRaw( - Knex (query builder): knex.raw( , whereRaw( For each hit, check: is user input CONCATENATED into the string, or passed as a BINDING/parameter? Concatenated = vulnerable; bound = fine. "LOOKS LIKE ORM BUT IS INJECTABLE" — EXAMPLE AND FIX: VULNERABLE (raw fragment with concatenation, still "using the ORM"): // Sequelize User.findAll({ where: Sequelize.literal("age > " + req.query.age) }); // or Laravel User::whereRaw("age > " . $request->age)->get(); // or Rails User.where("age > #{params[:age]}") Here req.query.age is concatenated into a raw SQL fragment. age = "0 OR 1=1" (or "0); DROP TABLE users--" depending on context) injects, even though no obvious db.query(...) string is present. The ORM didn't save you because you bypassed its binding. FIX (use bindings / parameter placeholders): // Sequelize — bound replacement User.findAll({ where: Sequelize.literal("age > :age"), replacements: { age: Number(req.query.age) } }); // simpler: stay in the structured API, which binds automatically: User.findAll({ where: { age: { [Op.gt]: Number(req.query.age) } } }); // Laravel User::whereRaw("age > ?", [ (int) $request->age ])->get(); // Rails User.where("age > ?", params[:age].to_i) Pass the value as a BINDING (or use the structured query API), and additionally validate/cast numeric inputs. The raw fragment now contains only a placeholder; the value is data. ONE-LINE TAKEAWAY: Normal ORM calls bind automatically (safe); audit by grepping the raw-query/ raw-fragment methods, and fix each by passing values as bindings (or using the structured API) instead of concatenating.