Writing secure programs
Validating input, access levels in code, and testing for security.
Do this lesson in the simulatorMany 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))
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"))
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
- Add a
governorrole that can onlyview. Check a governor cannot delete. - Extend
safe_speedfrom the first cell to also reject an empty string and text with spaces. - Explain how validating input stops SQL injection.