Secure software development using Python
Claudio Salazar
About me
- 8 years developing in Python
- Expertise in:
- web scraping
- vulnerability research
- secure software development
Agenda
- Designing a plugin system
- Testing to ensure security guarantees
Designing a plugin system
Requirements
- Plugins are Python files
- Prevent access to application's code (RCE)
- Prevent access to network resources (SSRF)
- Prevent side-effects of plugin executions (data)
- Prevent exhausting system resources (DoS)
- Prevent to run forever (DoS)
- Prevent side-effects of the solution
Solution
We're going to use a docker container through Docker SDK for Python
Solution
import docker
client = docker.from_env()
input_including_plugin_code = ...
container = client.containers.run(
"my-image",
input_including_plugin_code,
detach=True,
)
- Prevent access to application's code
- Prevent side-effects of plugin executionsÂ
Solution
...
container = client.containers.run(
"my-image",
input_including_plugin_code,
detach=True,
network_disabled=True,
)
Prevent access to network resources
Solution
...
container = client.containers.run(
"my-image",
input_including_plugin_code,
detach=True,
network_disabled=True,
nano_cpus=25 * 10**7,
mem_limit="128m",
)
Prevent exhausting system resources
Solution
import requests
...
MAX_SECONDS_TO_RUN = 10
try:
container.wait(timeout=MAX_SECONDS_TO_RUN)
except requests.exceptions.ConnectionError:
container.stop(timeout=0)
Prevent to run forever (DoS)
Solution
import requests
...
MAX_SECONDS_TO_RUN = 10
try:
container.wait(timeout=MAX_SECONDS_TO_RUN)
except requests.exceptions.ConnectionError:
container.stop(timeout=0)
# process container output
...
container.remove()
Prevent side-effects of the solution
Testing to ensure security guarantees
Let's create an app where
a user have orders
Defining an Order model
from django.contrib.auth import get_user_model
from django.db import models
class Order(models.Model):
price = models.IntegerField()
user = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE
)
secret_code = models.CharField(max_length=20)
Django settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Defining the endpoint
from rest_framework import serializers, viewsets
from .models import Order
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ["id", "price"]
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return Order.objects.filter(user=self.request.user)
serializer_class = OrderSerializer
Current solution
- Only authenticated users can access it
- Order's secret_code is never exposed
- Orders can be only listed and retrieved
- Users can only access their own orders
Code changes over time?
Test #1
def test_reject_anonymous_request(api_client):
orders_url = reverse("orders-list")
res = api_client(anon=True).get(orders_url)
assert res.status_code == status.HTTP_403_FORBIDDEN
Test the defaults that you rely on
6 months later
diff --git a/settings.py b/settings.py
index d41ee09..0aa5d51 100644
--- a/settings.py
+++ b/settings.py
@@ -1,5 +1,5 @@
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
- "rest_framework.permissions.IsAuthenticated",
+ "rest_framework.permissions.AllowAny",
]
}
A new decision changes the default policy
Test #2
def test_secret_code_is_not_returned(api_client):
user = UserFactory.create()
order = OrderFactory.create(user=user)
orders_url = reverse("orders-detail", args=(order.id,))
res = api_client(user).get(orders_url)
assert "secret_code" not in res.json()
Test you're not exposing sensitive data
8 months later
diff --git a/demo/orders/views.py b/demo/orders/views.py
index f42cec0..38e473d 100644
--- a/demo/orders/views.py
+++ b/demo/orders/views.py
@@ -6,7 +6,7 @@ from .models import Order
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
- fields = ["id", "price"]
+ fields = "__all__"
There are more fields to expose
Test #3
def test_only_list_retrieve(api_client):
user = UserFactory.create()
orders_url = reverse("orders-list")
res = api_client(user).options(orders_url)
assert res._headers["allow"][1] == "GET, HEAD, OPTIONS"
Test you only expose desired methods
1 year later
diff --git a/demo/orders/views.py b/demo/orders/views.py
index f42cec0..a4b9da0 100644
--- a/demo/orders/views.py
+++ b/demo/orders/views.py
@@ -9,7 +9,7 @@ class OrderSerializer(serializers.ModelSerializer):
fields = ["id", "price"]
-class OrderViewSet(viewsets.ReadOnlyModelViewSet):
+class OrderViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Order.objects.filter(user=self.request.user)
Let's get ModelViewSet for every endpoint
Test #4
def test_user_cant_access_another_user_order(api_client):
user_1 = UserFactory.create()
user_2 = UserFactory.create()
order = OrderFactory.create(user=user_1)
orders_url = reverse("orders-detail", args=(order.id,))
res = api_client(user_2).get(orders_url)
assert res.status_code == status.HTTP_404_NOT_FOUND
Test that everything works as expected
IDOR
More cases? Parametrize
@pytest.mark.parametrize(
"username,authenticated,status_code",
[
("anon", False, 403),
("user_1", True, 404),
("user_from_other_group", True, 404),
...,
],
)
def test_access_to_resource(username, authenticated, status_code):
user = get_user(username, authenticated)
res = api_client(user).get("/endpoint")
assert res.status_code == status_code
The goal
Q & A
Gracias!
Secure software development using Python
By csalazar
Secure software development using Python
PyDayChile 2020 - 15min
- 1,099