Writing secure programs

Validating input, access levels in code, and testing for security.

F11.8Cyber securityGCSE15 min

Covers OCR J277 1.4.2 Identifying and preventing vulnerabilities · AQA 8525 3.6.3 Methods to detect and prevent cyber security threats · Edexcel 1CP2 5.3.2 Protecting digital systems

Do this lesson in the simulator

Many attacks succeed because a program trusted something it should not have: input that was really an attack, or a user who should not have had access. Security is partly the programmer's job. This lesson brings together the defensive habits from earlier modules and applies them to keeping a system safe.

Never trust input

Every input is a possible attack. SQL injection (lesson F11.4) works because a program drops user input straight into a database query. The defence is validation (lesson F6.1) and keeping input separate from commands:

  • check input is the right type, length and range before using it;
  • reject or clean anything unexpected;
  • never build a command by gluing user input into it.
Two routes from user input to a database: glued straight into the query, or checked by validation and then sent separately from the queryglued into the commanduser inputglued straightinto the querytextthe query textis sent to thedatabasethe databaseruns the queryvalidated, then kept separateuser inputvalidation:type, length,rangethe query, andthe input, sentseparatelythe databaseruns the queryfails a check: rejected
The two routes from the list above, step by step. In the top row the input is glued into the query text; in the bottom row it is validated first, then sent separately from the query.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def safe_speed(text):
    if not text.isdigit():
        return None                          # not a number at all
    value = int(text)
    if 0 <= value <= 100:
        return value
    return None                              # out of range

for entry in ["50", "-10", "999", "go fast", "0"]:
    print(entry, "->", safe_speed(entry))

Run this in the simulator

The five test entries from the lesson's code going through two checks, digits only and 0 to 100; each is stopped at the check it fails or reaches the program as a numbercheck 1only digits?check 20 to 100?used"50"50"-10"rejected: None"999"rejected: None"go fast"rejected: None"0"0
Each entry is stopped by the first check it fails, and only 50 and 0 reach the program. "-10" is caught by check 1, not check 2, because the minus sign is not a digit, so the range check never sees it.

Access levels in code

User access levels (lesson F11.7) are enforced in the program: before doing something, check the user is allowed. A student can read their grades; only a teacher can change them.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

can_do = {"student": ["view"], "teacher": ["view", "edit"], "admin": ["view", "edit", "delete"]}

def allowed(role, action):
    return action in can_do.get(role, [])

for role in ["student", "teacher", "admin"]:
    print(role, "can edit:", allowed(role, "edit"))

Run this in the simulator

More defensive habits

  • Fail safely: if something goes wrong, stop in a safe state and reveal nothing. BugBot's firmware stops the motors if your program crashes (lesson F9.8).
  • Give the least access needed: a program, like a person, should only have the permissions its job requires.
  • Keep secrets out of code: never write a password or key straight into a program that others can read.
  • Log what happens: a record of who did what helps you spot an attack and understand it afterwards.
  • Keep software updated: most attacks use holes that were already fixed in an update.

Testing for security

The testing from lesson F6.3 applies here too. Try the boundary and the invalid: empty input, huge input, input full of symbols, someone acting above their access level. A secure program is one that has been attacked by its own tests first.

Task: an access checker

Write allowed(role, action) using the can_do table, returning True or False. For each (user, role, action) in requests, print <user> (<role>) <action>: allowed or : denied. Validate first: if the role is not in can_do, print <user> (<role>) <action>: unknown role instead. At the end print denied or blocked: <n>.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

can_do = {"student": ["view"], "teacher": ["view", "edit"], "admin": ["view", "edit", "delete"]}
# user, role, action
requests = [
    ("Sam", "student", "view"),
    ("Sam", "student", "edit"),
    ("Mr Lee", "teacher", "edit"),
    ("root", "admin", "delete"),
    ("ghost", "hacker", "delete"),
]

Challenges

  1. Add a governor role that can only view. Check a governor cannot delete.
  2. Extend safe_speed from the first cell to also reject an empty string and text with spaces.
  3. Explain how validating input stops SQL injection.