ЁЯПл The SchoolтА║ЁЯЫбя╕П SecurityтА║ЁЯТЙ рдзрдбрд╛ 01 тАФ Injection: data рдХрдзреАрд╣реА command рдмрдиреВ рдирдпреЗ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯТЙ рдзрдбрд╛ 01 тАФ Injection: data рдХрдзреАрд╣реА command рдмрдиреВ рдирдпреЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 16 рдкреИрдХреА рдзрдбрд╛ 01 ┬╖ рдкреБрдвреЗ: lesson-02-xss-csp


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рд╢рд╛рд│реЗрдЪреНрдпрд╛ security office рдЪреА рдкрд╣рд┐рд▓реА рдЦрд┐рдбрдХреА: рдЕрд░реНрдЬ рдЦрд┐рдбрдХреА (forms desk). рд╣рд╛ рдзрдбрд╛ рд╢рд┐рдХрд╡рддреЛ рддреЛ рдПрдХрдЪ рдирд┐рдпрдо web security рдордзрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рдЬреБрдирд╛ рдирд┐рдпрдо рдЖрд╣реЗ тАФ user рдЬреЗ рдЯрд╛рдЗрдк рдХрд░рддреЛ рддреЗ data рдЕрд╕рддреЗ, рдХрдзреАрд╣реА command рдЪрд╛ рднрд╛рдЧ рдирд╕рддреЗ. рддреБрдореНрд╣реА рдкрд╛рд╣рд╛рд▓: text рдЬреЛрдбреВрди SQL рдордзреНрдпреЗ рдмрдирд╡рд▓реЗрд▓реЗ grade lookup, рдПрдХ crafted input рдЬреНрдпрд╛рдореБрд│реЗ рддреЗ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдЪреА grade рдкрд░рдд рджреЗрддреЗ, рдЖрдгрд┐ рдПрдХрд╛ рдУрд│реАрдЪрд╛ рдЙрдкрд╛рдп: parameterised query. рд╕рдВрдкреВрд░реНрдг рдХреЛрд░реНрд╕рднрд░ рддреБрдореНрд╣реА рд╡рд╛рдкрд░рд╛рд▓ рддреНрдпрд╛ рдЦрд▒реНрдпрд╛ files:

ЁЯЫбя╕П рдмрдЪрд╛рд╡рд╛рддреНрдордХ рдЖрдгрд┐ рд╢реИрдХреНрд╖рдгрд┐рдХ. рдпрд╛ рдХреЛрд░реНрд╕рдордзрд▓реА рдкреНрд░рддреНрдпреЗрдХ рдХрдордХреБрд╡рдд рдЬрд╛рдЧрд╛ рдпрд╛ repo рдордзрд▓реНрдпрд╛рдЪ local, in-memory data рд╡рд░ рджрд╛рдЦрд╡рд▓реА рдЖрд╣реЗ тАФ memory рдордзрд▓реА рдПрдХ SQLite table рдЖрдгрд┐ рд╕рд╛рдзреНрдпрд╛ strings. рдХрд╛рд╣реАрд╣реА network рд▓рд╛ рдХрд┐рдВрд╡рд╛ рдЦрд▒реНрдпрд╛ system рд▓рд╛ рд╕реНрдкрд░реНрд╢ рдХрд░рдд рдирд╛рд╣реА. рдЬреЗ рд╢рд┐рдХрд╛рд▓ рддреЗ рддреБрдордЪреНрдпрд╛ рдорд╛рд▓рдХреАрдЪреНрдпрд╛ рдХрд┐рдВрд╡рд╛ рдЬреНрдпрд╛рд╡рд░ рдХрд╛рдо рдХрд░рдгреНрдпрд╛рдЪреА рдкрд░рд╡рд╛рдирдЧреА рдЖрд╣реЗ рдЕрд╢рд╛ systems рдЪреЗ рд╕рдВрд░рдХреНрд╖рдг рдХрд░рдгреНрдпрд╛рд╕рд╛рдареА рд╡рд╛рдкрд░рд╛. рддреБрдордЪреНрдпрд╛ рдорд╛рд▓рдХреАрдЪреА рдирд╕рд▓реЗрд▓реА system test рдХрд░рдгреЗ рдмрд╣реБрддреЗрдХ рджреЗрд╢рд╛рдВрдд рдХрд╛рдпрджреНрдпрд╛рд╡рд┐рд░реБрджреНрдз рдЖрд╣реЗ.

ЁЯОТ рд╕реБрд░реВ рдХрд░рдгреНрдпрд╛рдЖрдзреА: рддреБрдореНрд╣рд╛рд▓рд╛ рдлрдХреНрдд Python 3 рд▓рд╛рдЧрддреЗ, рджреБрд╕рд░реЗ рдХрд╛рд╣реА рдирд╛рд╣реА тАФ pip install рдирд╛рд╣реА, cloud account рдирд╛рд╣реА. рдЦрд▒реНрдпрд╛ account рд╡рд░ рдЕрд╕реЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реНрдпрд╛ commands рд╕рд╛рдареА рддреБрдордЪрд╛ рд╕реНрд╡рддрдГрдЪрд╛ рдЦрд░рд╛ database, framework рдХрд┐рдВрд╡рд╛ cloud account рд▓рд╛рдЧрддреЛ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдЪреНрдпрд╛ office рдордзреНрдпреЗ рдПрдХ рдХрд╛рд░рдХреВрди рдЖрд╣реЗ рдЬреЛ records room рдордзреВрди grades рдЖрдгреВрди рджреЗрддреЛ. рддреБрдореНрд╣реА рдПрдХ рдЪрд┐рдареНрдареА рднрд░рддрд╛, рдЖрдгрд┐ рдХрд╛рд░рдХреВрди рддреА рдЪрд┐рдареНрдареА records room рд▓рд╛ рдореЛрдареНрдпрд╛рдиреЗ рд╡рд╛рдЪреВрди рджрд╛рдЦрд╡рддреЛ:

"рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА katrina рдЪреА grade рдЖрдгрд╛."

рдЖрддрд╛ рдПрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдирд╛рд╡рд╛рдЪреНрдпрд╛ рдЪреМрдХрдЯреАрдд рд╣реЗ рд▓рд┐рд╣рд┐рддреЛ:

katrina тАФ рдЖрдгрд┐ рдмрд╛рдХреА рд╕рдЧрд│реНрдпрд╛рдВрдЪреНрдпрд╛ grades рдкрдг рдЖрдгрд╛

рдХрд╛рд░рдХреВрди рдкреВрд░реНрдг рдЪрд┐рдареНрдареА рдореЛрдареНрдпрд╛рдиреЗ рд╡рд╛рдЪрддреЛ. Records room рд▓рд╛ рдПрдХрдЪ рд▓рд╛рдВрдмрд▓рдЪрдХ рдЖрджреЗрд╢ рдРрдХреВ рдпреЗрддреЛ, рдЖрдгрд┐ рддреА рддреЛ рдкреВрд░реНрдгрдкрдгреЗ рдкрд╛рд│рддреЗ. рдкреНрд░рддреНрдпреЗрдХ grade рдЦреЛрд▓реАрдмрд╛рд╣реЗрд░ рдЬрд╛рддреЗ.

Security office рдЪрд┐рдареНрдареА рджреБрд░реБрд╕реНрдд рдХрд░рддреЗ. рдЖрддрд╛ рдкреНрд░рд╢реНрди рдЪрд┐рдареНрдареАрд╡рд░ рдЫрд╛рдкрд▓реЗрд▓рд╛ рдЕрд╕рддреЛ: "рдпрд╛ рдЪреМрдХрдЯреАрдд рдирд╛рд╡ рд▓рд┐рд╣рд┐рд▓реЗрд▓реНрдпрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдЪреА grade рдЖрдгрд╛." рдЪреМрдХрдЯ рдлрдХреНрдд рдЪреМрдХрдЯ рдЕрд╕рддреЗ. рддрд┐рдереЗ рдЬреЗ рдХрд╛рд╣реА рд▓рд┐рд╣рд┐рд▓реЗ рдЕрд╕реЗрд▓ тАФ рдЕрдЧрджреА "рдЖрдгрд┐ рдмрд╛рдХреА рд╕рдЧрд│реНрдпрд╛рдВрдЪреНрдпрд╛ grades рдкрдг рдЖрдгрд╛" тАФ рддреЗ рдПрдХ рдирд╛рд╡ рдореНрд╣рдгреВрдирдЪ рд╡рд╛рдЪрд▓реЗ рдЬрд╛рддреЗ. рддреНрдпрд╛ рдирд╛рд╡рд╛рдЪрд╛ рдХреЛрдгрддрд╛рд╣реА рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдирд╛рд╣реА, рдореНрд╣рдгреВрди рдХрд╛рд╣реАрдЪ рдкрд░рдд рдпреЗрдд рдирд╛рд╣реА.

рддреА рдЫрд╛рдкрд▓реЗрд▓реА рдЪрд┐рдареНрдареА рдореНрд╣рдгрдЬреЗ parameterised query. рдкреНрд░рд╢реНрди рдЖрдгрд┐ рдЙрддреНрддрд░рд╛рдЪреА рдЪреМрдХрдЯ рд╡реЗрдЧрд╡реЗрдЧрд│реЗ рдкреНрд░рд╡рд╛рд╕ рдХрд░рддрд╛рдд, рддреНрдпрд╛рдореБрд│реЗ рдЪреМрдХрдЯ рдкреНрд░рд╢реНрди рдХрдзреАрдЪ рдмрджрд▓реВ рд╢рдХрдд рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    u["ЁЯзТ input<br/>x' OR '1'='1"]
    subgraph bad["тЭМ glued into the text"]
      s1["SELECT тАж WHERE pupil = 'x' OR '1'='1'"]
      r1["3 rows тАФ every pupil"]
      s1 --> r1
    end
    subgraph good["тЬЕ sent as a parameter"]
      s2["SELECT тАж WHERE pupil = ?"]
      v2["value: x' OR '1'='1"]
      r2["0 rows тАФ no pupil has that name"]
      s2 --> r2
      v2 --> r2
    end
    u --> s1
    u --> v2

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l01

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдЪреБрдХреАрдЪреНрдпрд╛ рдкрджреНрдзрддреАрдиреЗ рдмрдирд╡рд▓реЗрд▓реЗ рдПрдХ lookup рдкреВрд░реНрдг table рд╡рд╛рдЯреВрди рдЯрд╛рдХреВ рд╢рдХрддреЗ: grades, emails, password hashes. Injection рд▓рд╛ рдЦрд╛рд╕ tools рд▓рд╛рдЧрдд рдирд╛рд╣реАрдд тАФ рдлрдХреНрдд рдПрдХ text box. рдЖрдгрд┐ рдЙрдкрд╛рдп рд╕реНрд╡рд╕реНрдд рдЖрд╣реЗ: рддреАрдЪ code рдЪреА рдУрд│, placeholder рд╕рд╣ рд▓рд┐рд╣рд┐рд▓реЗрд▓реА. рд╣рд╛рддрд╛рдиреЗ quotes escape рдХрд░рдгреЗ рд╣рд╛ рдЙрдкрд╛рдп рдирд╛рд╣реА. рд╣рд╛рддрд╛рдиреЗ рдХреЗрд▓реЗрд▓реНрдпрд╛ escaping рд▓рд╛ рдЪреБрдХрд╡рдгреНрдпрд╛рдЪреЗ рдЕрдиреЗрдХ рдорд╛рд░реНрдЧ рдЖрд╣реЗрдд (рджреБрд╕рд░реА quote рдЪрд┐рдиреНрд╣реЗ, encodings, quotes рдирд╕рд▓реЗрд▓реЗ numbers). Parameter рд╣реА рд╕рдорд╕реНрдпрд╛ рдореБрд│рд╛рдкрд╛рд╕реВрди рдХрд╛рдвреВрди рдЯрд╛рдХрддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

sec/web.py рдордзрд▓реЗ grades_db() рддреАрди рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрд╕рд╣ рдПрдХ in-memory SQLite table рдмрдирд╡рддреЗ: Katrina (A+), Dipika (B+) рдЖрдгрд┐ Aishwarya (A). unsafe_lookup(db, pupil) input SQL text рдордзреНрдпреЗ рдЬреЛрдбрддреЗ. safe_lookup(db, pupil) рддреЛ parameter рдореНрд╣рдгреВрди рдкрд╛рдард╡рддреЗ: db.execute("тАж WHERE pupil = ?", (pupil,)). sec/demo.py рдордзрд▓реЗ injection() рджреЛрдиреНрд╣реАрдВрдирд╛ рд╕рд╛рдзреНрдпрд╛ рдирд╛рд╡рд╛рдиреЗ рдЖрдгрд┐ crafted input рдиреЗ call рдХрд░рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 sec/demo.py injection
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from web import grades_db, unsafe_lookup, safe_lookup
db = grades_db()
for text in ("dipika", "x' OR '1'='1", "nobody' OR pupil LIKE '%a%", "katrina'--"):
    print(f"{text!r:<30} unsafe тЖТ {len(unsafe_lookup(db, text))} rows ┬╖ safe тЖТ {len(safe_lookup(db, text))} rows")
EOF
python3 sec/test_sec.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

injection рд╣реЗ print рдХрд░рддреЗ:

тФАтФА the grade lookup asks for one pupil's grade
   normal input 'katrina'           unsafe тЖТ [('katrina', 'A+')]  ┬╖  safe тЖТ [('katrina', 'A+')]
   crafted input "x' OR '1'='1"     unsafe тЖТ [('katrina', 'A+'), ('dipika', 'B+'), ('aishwarya', 'A')]
                                       safe   тЖТ []
   the fix is the same everywhere: send data as parameters, never glue it into the command (SQL, shell, LDAP, templates)

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

'dipika'                       unsafe тЖТ 1 rows ┬╖ safe тЖТ 1 rows
"x' OR '1'='1"                 unsafe тЖТ 3 rows ┬╖ safe тЖТ 0 rows
"nobody' OR pupil LIKE '%a%"   unsafe тЖТ 3 rows ┬╖ safe тЖТ 0 rows
"katrina'--"                   unsafe тЖТ 1 rows ┬╖ safe тЖТ 0 rows

Tests 16/16 passed рдиреЗ рд╕рдВрдкрддрд╛рдд.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рд╕рд╛рдзреНрдпрд╛ input рдиреЗ рджреЛрдиреНрд╣реА lookups рд╕рд╛рд░рдЦреЗрдЪ рджрд┐рд╕рддрд╛рдд тАФ рдореНрд╣рдгреВрдирдЪ рд╣рд╛ bug code review рдЖрдгрд┐ tests рдордзреВрди рдЯрд┐рдХрддреЛ. Crafted input рдиреЗ рдЬреЛрдбрд▓реЗрд▓реА query рддрд┐рдиреНрд╣реА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдкрд░рдд рджреЗрддреЗ, рдЖрдгрд┐ katrina'-- рддрд░ query рдЪрд╛ рдЙрд░рд▓реЗрд▓рд╛ рднрд╛рдЧ comment рдордзреНрдпреЗ рдмрджрд▓рддреЛ. Parameterised query рдкреНрд░рддреНрдпреЗрдХ crafted input рд╕рд╛рдареА рдХрд╛рд╣реАрдЪ рдкрд░рдд рджреЗрдд рдирд╛рд╣реА: input рдиреЗрд╣рдореА рдлрдХреНрдд рдПрдХ рдирд╛рд╡рдЪ рд╣реЛрддрд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рд╕рд░реНрд╡рд╛рдд рдЬрд╛рд╕реНрдд рднреЗрдЯрдгрд╛рд▒реНрдпрд╛ рднрд╛рд╖рд╛рдВрдордзрд▓рд╛ рд╣рд╛рдЪ рдЙрдкрд╛рдп. PostgreSQL (psycopg) рд╕рд╣ Python %s placeholders рд╡рд╛рдкрд░рддреЗ тАФ Python рдЪрд╛ % operator рдирд╛рд╣реА:

# тЬЕ the value travels separately; psycopg never pastes it into the SQL text
cur.execute("SELECT pupil, grade FROM grades WHERE pupil = %s", (pupil,))

pg рд╕рд╣ Node.js рдХреНрд░рдорд╛рдВрдХрд┐рдд placeholders рд╡рд╛рдкрд░рддреЗ:

const { rows } = await pool.query(
  'SELECT pupil, grade FROM grades WHERE pupil = $1', [pupil]);

JDBC рд╕рд╣ Java PreparedStatement рд╡рд╛рдкрд░рддреЗ:

try (PreparedStatement ps = conn.prepareStatement(
        "SELECT pupil, grade FROM grades WHERE pupil = ?")) {
    ps.setString(1, pupil);
    try (ResultSet rs = ps.executeQuery()) { /* тАж */ }
}

ORM (SQLAlchemy 2.x) рддреБрдордЪреНрдпрд╛рд╕рд╛рдареА parameter рдмрдирд╡рддреЛ тАФ рдЖрдгрд┐ raw SQL рд▓рд╛рдЧрд▓реЗ рддрд░ рддреЗ bind рдХрд░рд╛:

from sqlalchemy import select, text
rows = session.execute(select(Grade).where(Grade.pupil == pupil)).scalars().all()
rows = session.execute(text("SELECT grade FROM grades WHERE pupil = :p"), {"p": pupil}).all()

User рдЬреНрдпрд╛ column рдиреЗ sort рдХрд░реВ рд╢рдХрддреЛ рддреЛ parameter рдмрдиреВ рд╢рдХрдд рдирд╛рд╣реА тАФ рддреЛ allow-list рдордзреВрди рдЬреЛрдбрд╛:

SORTABLE = {"name": "pupil", "grade": "grade"}
column = SORTABLE.get(request.args.get("sort"), "pupil")   # never the raw input

рдЖрдгрд┐ рдпрд╛рдЪ рдирд┐рдпрдорд╛рдЪреА shell рдЖрд╡реГрддреНрддреА тАФ arguments рдЪреА list, shell рдирд╛рд╣реА:

subprocess.run(["convert", f"{safe_name}.png", "out.jpg"], check=True)   # no shell=True

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рддреБрдордЪреНрдпрд╛ code рдордзреНрдпреЗ +, f", .format( рдХрд┐рдВрд╡рд╛ template strings рдиреЗ рдмрдирд╡рд▓реЗрд▓реЗ SQL, рдЖрдгрд┐ shell=True рд╢реЛрдзрд╛. Bandit (Python) рдЖрдгрд┐ Semgrep rules рд╕рд╛рд░рдЦреЗ linters рддреНрдпрд╛рддрд▓реЗ рдмрд╣реБрддреЗрдХ рд╢реЛрдзрддрд╛рдд. App рдЪреНрдпрд╛ database user рд▓рд╛ рдлрдХреНрдд рд▓рд╛рдЧрдгрд╛рд░реЗрдЪ рдЕрдзрд┐рдХрд╛рд░ рджреНрдпрд╛, рдореНрд╣рдгрдЬреЗ рд╕реБрдЯрд▓реЗрд▓рд╛ bug рдХрдореА рд╡рд╛рдЪреВ рд╢рдХреЗрд▓.

тПня╕П рдкреБрдвреЗ

Injection рд╡рд╛рдИрдЯ input server рдЪреНрдпрд╛ commands рдХрдбреЗ рдкрд╛рдард╡рддреЗ. рдкреБрдврдЪреА рдХрдордХреБрд╡рдд рдЬрд╛рдЧрд╛ рддреЛ рдЗрддрд░ рд▓реЛрдХрд╛рдВрдЪреНрдпрд╛ browsers рдХрдбреЗ рдкрд╛рдард╡рддреЗ: notice board рд╡рд░рдЪреА рдПрдХ comment рдЬреА code рдореНрд╣рдгреВрди рдЪрд╛рд▓рддреЗ.

git checkout lesson-02-xss-csp

ЁЯТЙ Lesson 01 тАФ Injection: data must never become a command

ЁЯУН You are here: Lesson 01 of 16 ┬╖ Next: lesson-02-xss-csp


ЁЯУж What's in this branch

The first desk of the school's security office: the forms desk. The one rule this lesson teaches is the oldest rule in web security тАФ what a user types is data, never part of a command. You see a grade lookup built by gluing text into SQL, a crafted input that makes it return every pupil's grade, and the one-line fix: a parameterised query. Real files you will use all the way through:

ЁЯЫбя╕П Defensive and educational. Every weakness in this course is shown on local, in-memory data inside this repo тАФ an SQLite table in memory and plain strings. Nothing touches a network or a real system. Use what you learn to protect systems you own or are allowed to work on. Testing a system you do not own is against the law in most countries.

ЁЯОТ Before you start: you need Python 3 and nothing else тАФ no pip install, no cloud account. Commands marked on a real account need a real database, framework or cloud account of your own.

ЁЯзТ Explain like I'm 5

The school office has a clerk who fetches grades from the records room. You fill in a slip, and the clerk reads the slip out loud to the records room:

"Bring the grade of pupil katrina."

Now a pupil writes this in the name box:

katrina тАФ and also bring everyone else's grades

The clerk reads the whole slip out loud. The records room hears one long order, and it obeys all of it. Every grade leaves the room.

The security office fixes the slip. The question is now printed on the slip: "Bring the grade of the pupil named in this box." The box is only a box. Whatever is written there тАФ even "and also bring everyone else's grades" тАФ is read as a name. No pupil has that name, so nothing comes back.

That printed slip is a parameterised query. The question and the answer box travel separately, so the box can never change the question.

ЁЯЧ║я╕П Diagram

flowchart LR
    u["ЁЯзТ input<br/>x' OR '1'='1"]
    subgraph bad["тЭМ glued into the text"]
      s1["SELECT тАж WHERE pupil = 'x' OR '1'='1'"]
      r1["3 rows тАФ every pupil"]
      s1 --> r1
    end
    subgraph good["тЬЕ sent as a parameter"]
      s2["SELECT тАж WHERE pupil = ?"]
      v2["value: x' OR '1'='1"]
      r2["0 rows тАФ no pupil has that name"]
      s2 --> r2
      v2 --> r2
    end
    u --> s1
    u --> v2

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l01

тЭУ What

ЁЯдФ Why

Because one lookup built the wrong way can hand out a whole table: grades, emails, password hashes. Injection needs no special tools тАФ only a text box. And the fix is cheap: the same line of code, written with a placeholder. Escaping quotes by hand is not the fix. There are many ways around hand-made escaping (other quote characters, encodings, numbers with no quotes). The parameter removes the problem at its root.

ЁЯФз How (in this repo)

grades_db() in sec/web.py makes an in-memory SQLite table with three pupils: Katrina (A+), Dipika (B+) and Aishwarya (A). unsafe_lookup(db, pupil) glues the input into the SQL text. safe_lookup(db, pupil) sends it as a parameter: db.execute("тАж WHERE pupil = ?", (pupil,)). injection() in sec/demo.py calls both with a normal name and with the crafted input.

ЁЯзк Try it

python3 sec/demo.py injection
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from web import grades_db, unsafe_lookup, safe_lookup
db = grades_db()
for text in ("dipika", "x' OR '1'='1", "nobody' OR pupil LIKE '%a%", "katrina'--"):
    print(f"{text!r:<30} unsafe тЖТ {len(unsafe_lookup(db, text))} rows ┬╖ safe тЖТ {len(safe_lookup(db, text))} rows")
EOF
python3 sec/test_sec.py

тЬЕ Verify тАФ what you should see

injection prints:

тФАтФА the grade lookup asks for one pupil's grade
   normal input 'katrina'           unsafe тЖТ [('katrina', 'A+')]  ┬╖  safe тЖТ [('katrina', 'A+')]
   crafted input "x' OR '1'='1"     unsafe тЖТ [('katrina', 'A+'), ('dipika', 'B+'), ('aishwarya', 'A')]
                                       safe   тЖТ []
   the fix is the same everywhere: send data as parameters, never glue it into the command (SQL, shell, LDAP, templates)

Your snippet prints:

'dipika'                       unsafe тЖТ 1 rows ┬╖ safe тЖТ 1 rows
"x' OR '1'='1"                 unsafe тЖТ 3 rows ┬╖ safe тЖТ 0 rows
"nobody' OR pupil LIKE '%a%"   unsafe тЖТ 3 rows ┬╖ safe тЖТ 0 rows
"katrina'--"                   unsafe тЖТ 1 rows ┬╖ safe тЖТ 0 rows

The tests end with 16/16 passed.

ЁЯПБ What you just proved

With normal input, both lookups look the same тАФ that is why this bug survives code review and tests. With crafted input, the glued query returns all three pupils, and katrina'-- even turns the rest of the query into a comment. The parameterised query returns nothing for every crafted input: the input was only ever a name.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ the same fix in the languages you meet most. Python with PostgreSQL (psycopg) uses %s placeholders тАФ not Python's % operator:

# тЬЕ the value travels separately; psycopg never pastes it into the SQL text
cur.execute("SELECT pupil, grade FROM grades WHERE pupil = %s", (pupil,))

Node.js with pg uses numbered placeholders:

const { rows } = await pool.query(
  'SELECT pupil, grade FROM grades WHERE pupil = $1', [pupil]);

Java with JDBC uses a PreparedStatement:

try (PreparedStatement ps = conn.prepareStatement(
        "SELECT pupil, grade FROM grades WHERE pupil = ?")) {
    ps.setString(1, pupil);
    try (ResultSet rs = ps.executeQuery()) { /* тАж */ }
}

An ORM (SQLAlchemy 2.x) builds the parameter for you тАФ and when you need raw SQL, bind it:

from sqlalchemy import select, text
rows = session.execute(select(Grade).where(Grade.pupil == pupil)).scalars().all()
rows = session.execute(text("SELECT grade FROM grades WHERE pupil = :p"), {"p": pupil}).all()

A column the user may sort by cannot be a parameter тАФ map it through an allow-list:

SORTABLE = {"name": "pupil", "grade": "grade"}
column = SORTABLE.get(request.args.get("sort"), "pupil")   # never the raw input

And the shell version of the same rule тАФ a list of arguments, no shell:

subprocess.run(["convert", f"{safe_name}.png", "out.jpg"], check=True)   # no shell=True

ЁЯПн Why this matters in production: search your code for SQL built with +, f", .format( or template strings, and for shell=True. Linters such as Bandit (Python) and Semgrep rules find most of them. Give the app's database user only the rights it needs, so a missed bug reads less.

тПня╕П Next

Injection sends bad input to the server's commands. The next weakness sends it to other people's browsers: a comment on the notice board that runs as code.

git checkout lesson-02-xss-csp
тЖР Course homeall lessonsNext тЖТxss csp

This page is the lesson's README from the lesson-01-injection branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.