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

ЁЯЫбя╕П рдзрдбрд╛ 13 тАФ Security: рдХреБрд▓реВрдкрдмрдВрдж рдиреЛрдВрджрд╡рд╣реА

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 13 ┬╖ рдорд╛рдЧреАрд▓: lesson-12-performance-ops ┬╖ рдкреБрдвреАрд▓: lesson-14-advanced-sql

рднрд╛рдЧ 3 тАФ рдЕрдзрд┐рдХ рдЦреЛрд▓рд╛рдд. рдзрдбреЗ 01тАУ12 рдиреА рд░реЗрдХреЙрд░реНрдб рд░реВрдо рдмрд╛рдВрдзрд▓реА рдЖрдгрд┐ рдЪрд╛рд▓рд╡рд▓реА. рдзрдбреЗ 13тАУ18 рд╣реЗ interviews рдЖрдгрд┐ incidents рдордзреНрдпреЗ рд╡рд┐рдЪрд╛рд░рд▓реЗ рдЬрд╛рдгрд╛рд░реЗ рд╡рд┐рд╖рдп рдЖрд╣реЗрдд тАФ рдкреНрд░рддреНрдпреЗрдХ рддреНрдпрд╛рдЪ school.db рд╡рд░ рдкреНрд░рддреНрдпрдХреНрд╖ рдЪрд╛рд▓рддреЛ.


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

рдзрдбреЗ 01тАУ13, рдЖрдгрд┐ db/demo.py рдордзрд▓реЗ inject(): рдЯрд╛рдЗрдк рдХреЗрд▓реЗрд▓рд╛ roll number рдереЗрдЯ SQL рдордзреНрдпреЗ рдЪрд┐рдХрдЯрд╡рдгрд╛рд░рд╛ login, рддреЛрдЪ login parameter рд╕рд╣, рдЖрдгрд┐ DELETE рдирд╛рдХрд╛рд░рд▓рд╛ рдЬрд╛рдгрд╛рд░реЗ read-only connection.

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

рдЕрднрд┐рд▓реЗрдЦрдкрд╛рд▓ рддреБрдордЪреА рдЪрд┐рдареНрдареА рдореЛрдареНрдпрд╛рдиреЗ рд╡рд╛рдЪрддреЗ рдЖрдгрд┐ рддреНрдпрд╛рдд рдЬреЗ рд▓рд┐рд╣рд┐рд▓реЗ рдЖрд╣реЗ рддреЗрдЪ рдХрд░рддреЗ. рдЪрд┐рдареНрдареАрд╡рд░ "roll number 3A-02 рд╢реЛрдзрд╛" рдЕрд╕реЗрд▓, рддрд░ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрддрд░рд┐рдирд╛ рдорд┐рд│рддреЗ. рдкрдг рдХреЛрдгреА рд░рд┐рдХрд╛рдореНрдпрд╛ рдЬрд╛рдЧреЗрдд рд╕реНрд╡рддрдГрдЪреЗ рд╢рдмреНрдж рд▓рд┐рд╣рд┐рд▓реЗ рддрд░ тАФ "roll number x' OR '1'='1 рд╢реЛрдзрд╛"? рдореЛрдареНрдпрд╛рдиреЗ рд╡рд╛рдЪрд▓реНрдпрд╛рд╡рд░ рдЪрд┐рдареНрдареА рдЖрддрд╛ рдореНрд╣рдгрддреЗ "roll number x рд╢реЛрдзрд╛, рдХрд┐рдВрд╡рд╛ рдЬрд┐рдереЗ 1 рдмрд░реЛрдмрд░ 1 рдЖрд╣реЗ рддреЗ рд╕рдЧрд│реЗ" тАФ рдЖрдгрд┐ 1 рдиреЗрд╣рдореАрдЪ 1 рдмрд░реЛрдмрд░ рдЕрд╕рддреЛ. рдЕрднрд┐рд▓реЗрдЦрдкрд╛рд▓ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рджреЗрдКрди рдЯрд╛рдХрддреЗ. рд╣реЗрдЪ SQL injection.

рдЙрдкрд╛рдп рдореНрд╣рдгрдЬреЗ рд░рд┐рдХрд╛рдореНрдпрд╛ рдЬрд╛рдЧреА рдмрдВрдж рд▓рд┐рдлрд╛рдлрд╛ (?) рдЕрд╕рд▓реЗрд▓реА рдЪрд┐рдареНрдареА. рд▓рд┐рдлрд╛рдлреНрдпрд╛рдд рдЬреЗ рдЕрд╕реЗрд▓ рддреЗ рдиреЗрд╣рдореА рдлрдХреНрдд рд╢реЛрдзрд╛рдпрдЪреА рдПрдХ value рдЕрд╕рддреЗ тАФ рдЕрднрд┐рд▓реЗрдЦрдкрд╛рд▓ рдкрд╛рд│рддреЗ рдЕрд╕реЗ рд╢рдмреНрдж рдХрдзреАрдЪ рдирд╛рд╣реАрдд.

рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рд░рдХреБрдирд╛рд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рдХрд┐рд▓реНрд▓реА рдорд┐рд│рдд рдирд╛рд╣реА: reports рдХрд╛рд░рдХреБрдирд╛рд▓рд╛ рдлрдХреНрдд-рд╡рд╛рдЪрдгреНрдпрд╛рдЪрд╛ pass рдорд┐рд│рддреЛ.

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

flowchart LR
    typed["тМия╕П typed: x' OR '1'='1"]
    glued["тЭМ glued: WHERE roll_no = 'x' OR '1'='1'"]
    param["тЬЕ parameter: WHERE roll_no = ?  +  value"]
    leak["ЁЯТе all 5 pupils"]
    none["(no rows)"]
    typed --> glued --> leak
    typed --> param --> none

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдЬреЛ connect рдЭрд╛рд▓рд╛ рдЖрд╣реЗ рддреНрдпрд╛рдЪреНрдпрд╛рд╡рд░ database рд╡рд┐рд╢реНрд╡рд╛рд╕ рдареЗрд╡рддреЛ. рджреЛрди рджрд╢рдХрд╛рдВрдкрд╛рд╕реВрди injection рд╣рд╛ рд╕рд░реНрд╡рд╛рдд рд╕рд╛рдорд╛рдиреНрдп web рд╣рд▓реНрд▓реНрдпрд╛рдВрдкреИрдХреА рдПрдХ рдЖрд╣реЗ, рдЖрдгрд┐ рддреЛ рдПрдХрд╛ рд╕рд╡рдпреАрдиреЗ рдкреВрд░реНрдгрдкрдгреЗ рдерд╛рдВрдмрддреЛ: strings рдЪрд┐рдХрдЯрд╡реВрди рдХрдзреАрдЪ SQL рдмрдирд╡реВ рдирдХрд╛. Least privilege рдЗрддрд░ рдкреНрд░рддреНрдпреЗрдХ рдЪреБрдХреАрдЪреЗ рдиреБрдХрд╕рд╛рди рдорд░реНрдпрд╛рджрд┐рдд рдареЗрд╡рддреЗ.

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

db/demo.py рдордзрд▓реЗ inject() database рд▓рд╛ рдкреНрд░рддреНрдпрдХреНрд╖ рдорд┐рд│рд╛рд▓реЗрд▓реА рдЪрд┐рдХрдЯрд╡рд▓реЗрд▓реА query рдЫрд╛рдкрддреЗ, рддреА рдЪрд╛рд▓рд╡рддреЗ (рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдкрд░рдд рдпреЗрддреЗ), parameter рдЕрд╕рд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА рдЪрд╛рд▓рд╡рддреЗ (рдПрдХрд╣реА row рдирд╛рд╣реА), рдордЧ school.db read-only рдЙрдШрдбреВрди DELETE FROM grades рдХрд░реВрди рдкрд╛рд╣рддреЗ.

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

python3 db/demo.py inject
python3 - <<'EOF'
import sqlite3
c = sqlite3.connect("db/school.db")
for typed in ["3A-02", "x' OR '1'='1", "3A-02'; --"]:
    print(repr(typed), "тЖТ", c.execute("SELECT name FROM students WHERE roll_no = ?", (typed,)).fetchall())
EOF

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

рдЪрд┐рдХрдЯрд╡рд▓реЗрд▓реА query WHERE roll_no = 'x' OR '1'='1' рдЫрд╛рдкрддреЗ рдЖрдгрд┐ рдкрд╛рдЪрд╣реА рдирд╛рд╡реЗ рдкрд░рдд рджреЗрддреЗ; parameter (no rows) рджреЗрддреЛ; read-only connection attempt to write a readonly database рдореНрд╣рдгрддреЗ. Snippet рдордзреНрдпреЗ рдлрдХреНрдд 3A-02 рд▓рд╛ рдХрддрд░рд┐рдирд╛ рд╕рд╛рдкрдбрддреЗ.

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

рддреБрдореНрд╣реА injection рдЪрд╛рд▓рддрд╛рдирд╛ рджрд╛рдЦрд╡реВ рд╢рдХрддрд╛, рддреЗ рдХрд╛ рд╣реЛрддреЗ рд╣реЗ рд╕рдордЬрд╛рд╡реВ рд╢рдХрддрд╛, рдЖрдгрд┐ parameter рдиреЗ рддреЗ рдмрдВрдж рдХрд░реВ рд╢рдХрддрд╛ тАФ рдЖрдгрд┐ app рд▓рд╛ рдЕрд╕рд╛ pass рджреЗрдК рд╢рдХрддрд╛ рдЬреЛ рдХрд╛рд╣реАрдЪ рд▓рд┐рд╣реВ рд╢рдХрдд рдирд╛рд╣реА.

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

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: security reviews рдиреЗрдордХреЗ рд╣реЗрдЪ рддрдкрд╛рд╕рддрд╛рдд: string рдиреЗ рдмрдирд╡рд▓реЗрд▓рд╛ SQL рдирд╛рд╣реА, app / migrations / reports рд╕рд╛рдареА рд╡реЗрдЧрд│реЗ roles, TLS рдЖрд╡рд╢реНрдпрдХ, at rest encryption, vault рдордзреВрди secrets, рдЖрдгрд┐ superuser рдЪреНрдпрд╛ logins рд╡рд░ alert.

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

git checkout lesson-14-advanced-sql тАФ рдПрдХрд╣реА row рди рдЧрдорд╛рд╡рддрд╛ rankings рдЖрдгрд┐ reports.

ЁЯЫбя╕П Lesson 13 тАФ Security: the locked register

ЁЯУН You are here: Lesson 13 of 18 ┬╖ Previous: lesson-12-performance-ops ┬╖ Next: lesson-14-advanced-sql

Part 3 тАФ going deeper. Lessons 01тАУ12 built and ran the record room. Lessons 13тАУ18 are the topics interviews and incidents ask about тАФ each one runs for real on the same school.db.


ЁЯУж What's in this branch

Lessons 01тАУ13, plus inject() in db/demo.py: a login that glues the typed roll number into SQL, the same login with a parameter, and a read-only connection that is refused a DELETE.

ЁЯзТ Explain like I'm 5

The archivist reads your slip out loud and does exactly what it says. If the slip is "find roll number 3A-02", you get Katrina. But what if someone writes their own words into the blank тАФ "find roll number x' OR '1'='1"? Read out loud, the slip now says "find roll number x, or anything where 1 equals 1" тАФ and 1 always equals 1. The archivist hands over every pupil. That is SQL injection.

The fix is a slip with a sealed envelope in the blank (?). Whatever is inside the envelope is only ever a value to look up тАФ never words the archivist obeys.

And not every clerk gets every key: the reports clerk gets a read-only pass.

ЁЯЧ║я╕П Diagram

flowchart LR
    typed["тМия╕П typed: x' OR '1'='1"]
    glued["тЭМ glued: WHERE roll_no = 'x' OR '1'='1'"]
    param["тЬЕ parameter: WHERE roll_no = ?  +  value"]
    leak["ЁЯТе all 5 pupils"]
    none["(no rows)"]
    typed --> glued --> leak
    typed --> param --> none

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

тЭУ What

ЁЯдФ Why

Because the database trusts whoever is connected. Injection has been one of the most common web attacks for two decades, and it is fixed completely by one habit: never build SQL by gluing strings. Least privilege limits the damage of every other mistake.

ЁЯФз How (in this repo)

inject() in db/demo.py prints the glued query the database actually received, runs it (every pupil comes back), runs the parameterised version (no rows), then opens school.db read-only and tries DELETE FROM grades.

ЁЯзк Try it

python3 db/demo.py inject
python3 - <<'EOF'
import sqlite3
c = sqlite3.connect("db/school.db")
for typed in ["3A-02", "x' OR '1'='1", "3A-02'; --"]:
    print(repr(typed), "тЖТ", c.execute("SELECT name FROM students WHERE roll_no = ?", (typed,)).fetchall())
EOF

тЬЕ Verify тАФ what you should see

The glued query prints WHERE roll_no = 'x' OR '1'='1' and returns all five names; the parameter returns (no rows); the read-only connection says attempt to write a readonly database. In the snippet, only 3A-02 finds Katrina.

ЁЯПБ What you just proved

You can show an injection working, explain why, and close it with a parameter тАФ and you can give an app a pass that cannot write at all.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: security reviews check exactly this: no string-built SQL, separate roles for app / migrations / reports, TLS required, encryption at rest, secrets from a vault, and an alert on logins by the superuser.

тПня╕П Next

git checkout lesson-14-advanced-sql тАФ rankings and reports without losing a row.

тЖР Previousperformance opsNext тЖТadvanced sql

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