Writing secure programs

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

F11.8Cyber securityGCSE15 min

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.2) 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.
# 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

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.4 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.