Merge pull request #7016 from RasmusWL/django-rest-framework

Python: Model Django REST framework
This commit is contained in:
yoff
2021-11-12 14:27:56 +01:00
committed by GitHub
32 changed files with 1257 additions and 12 deletions

View File

@@ -0,0 +1 @@
db.sqlite3

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest

View File

@@ -0,0 +1,3 @@
argumentToEnsureNotTaintedNotMarkedAsSpurious
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
failures

View File

@@ -0,0 +1 @@
import experimental.meta.InlineTaintTest

View File

@@ -0,0 +1,23 @@
See README for `django-v2-v3` which described how the project was set up.
Since this test project uses models (and a DB), you generally need to run there 3 commands:
```
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
```
Then visit http://127.0.0.1:8000/
# References
- https://www.django-rest-framework.org/tutorial/quickstart/
# Editing data
To edit data you should add an admin user (will prompt for password)
```
python manage.py createsuperuser --email admin@example.com --username admin
```

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,50 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.exceptions import APIException
@api_view()
def normal_response(request): # $ requestHandler
# has no pre-defined content type, since that will be negotiated
# see https://www.django-rest-framework.org/api-guide/responses/
data = "data"
resp = Response(data) # $ HttpResponse responseBody=data
return resp
@api_view()
def plain_text_response(request): # $ requestHandler
# this response is not the standard way to use the Djagno REST framework, but it
# certainly is possible -- notice that the response contains double quotes
data = 'this response will contain double quotes since it was a string'
resp = Response(data, None, None, None, None, "text/plain") # $ HttpResponse mimetype=text/plain responseBody=data
resp = Response(data=data, content_type="text/plain") # $ HttpResponse mimetype=text/plain responseBody=data
return resp
################################################################################
# Cookies
################################################################################
@api_view
def setting_cookie(request):
resp = Response() # $ HttpResponse
resp.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
resp.set_cookie(key="key4", value="value") # $ CookieWrite CookieName="key4" CookieValue="value"
resp.headers["Set-Cookie"] = "key2=value2" # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
resp.cookies["key3"] = "value3" # $ CookieWrite CookieName="key3" CookieValue="value3"
resp.delete_cookie("key4") # $ CookieWrite CookieName="key4"
resp.delete_cookie(key="key4") # $ CookieWrite CookieName="key4"
return resp
################################################################################
# Exceptions
################################################################################
# see https://www.django-rest-framework.org/api-guide/exceptions/
@api_view(["GET", "POST"])
def exception_test(request): # $ requestHandler
data = "exception details"
# note: `code details` not exposed by default
code = "code details"
e1 = APIException(data, code) # $ HttpResponse responseBody=data
e2 = APIException(detail=data, code=code) # $ HttpResponse responseBody=data
raise e2

View File

@@ -0,0 +1,131 @@
from rest_framework.decorators import api_view, parser_classes
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.parsers import JSONParser
from django.urls import path
ensure_tainted = ensure_not_tainted = print
# function based view
# see https://www.django-rest-framework.org/api-guide/views/#function-based-views
@api_view(["POST"])
@parser_classes([JSONParser])
def test_taint(request: Request, routed_param): # $ requestHandler routedParameter=routed_param
ensure_tainted(routed_param) # $ tainted
ensure_tainted(request) # $ tainted
# Has all the standard attributes of a django HttpRequest
# see https://github.com/encode/django-rest-framework/blob/00cd4ef864a8bf6d6c90819a983017070f9f08a5/rest_framework/request.py#L410-L418
ensure_tainted(request.resolver_match.args) # $ tainted
# special new attributes added, see https://www.django-rest-framework.org/api-guide/requests/
ensure_tainted(
request.data, # $ tainted
request.data["key"], # $ tainted
# alias for .GET
request.query_params, # $ tainted
request.query_params["key"], # $ tainted
request.query_params.get("key"), # $ tainted
request.query_params.getlist("key"), # $ tainted
request.query_params.getlist("key")[0], # $ tainted
request.query_params.pop("key"), # $ tainted
request.query_params.pop("key")[0], # $ tainted
# see more detailed tests of `request.user` below
request.user, # $ tainted
request.auth, # $ tainted
# seems much more likely attack vector than .method, so included
request.content_type, # $ tainted
# file-like
request.stream, # $ tainted
request.stream.read(), # $ tainted
)
ensure_not_tainted(
# although these could technically be user-controlled, it seems more likely to lead to FPs than interesting results.
request.accepted_media_type,
# In normal Django, if you disable CSRF middleware, you're allowed to use custom
# HTTP methods, like `curl -X FOO <url>`.
# However, with Django REST framework, doing that will yield:
# `{"detail":"Method \"FOO\" not allowed."}`
#
# In the end, since we model a Django REST framework request entirely as a
# extension of a Django request, we're not easily able to remove the taint from
# `.method`.
request.method, # $ SPURIOUS: tainted
)
# --------------------------------------------------------------------------
# request.user
# --------------------------------------------------------------------------
#
# This will normally be an instance of django.contrib.auth.models.User
# (authenticated) so we assume that normally user-controlled fields such as
# username/email is user-controlled, but that password isn't (since it's a hash).
# see https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#fields
ensure_tainted(
request.user.username, # $ tainted
request.user.first_name, # $ tainted
request.user.last_name, # $ tainted
request.user.email, # $ tainted
)
ensure_not_tainted(request.user.password)
return Response("ok") # $ HttpResponse responseBody="ok"
# class based view
# see https://www.django-rest-framework.org/api-guide/views/#class-based-views
class MyClass(APIView):
def initial(self, request, *args, **kwargs): # $ requestHandler
# this method will be called before processing any request
ensure_tainted(request) # $ tainted
def get(self, request: Request, routed_param): # $ requestHandler routedParameter=routed_param
ensure_tainted(routed_param) # $ tainted
# request taint is the same as in function_based_view above
ensure_tainted(
request, # $ tainted
request.data # $ tainted
)
# same as for standard Django view
ensure_tainted(self.args, self.kwargs) # $ tainted
return Response("ok") # $ HttpResponse responseBody="ok"
# fake setup, you can't actually run this
urlpatterns = [
path("test-taint/<routed_param>", test_taint), # $ routeSetup="test-taint/<routed_param>"
path("ClassView/<routed_param>", MyClass.as_view()), # $ routeSetup="ClassView/<routed_param>"
]
# tests with no route-setup, but we can still tell that these are using Django REST
# framework
@api_view(["POST"])
def function_based_no_route(request: Request, possible_routed_param): # $ requestHandler routedParameter=possible_routed_param
ensure_tainted(
request, # $ tainted
possible_routed_param, # $ tainted
)
class ClassBasedNoRoute(APIView):
def get(self, request: Request, possible_routed_param): # $ requestHandler routedParameter=possible_routed_param
ensure_tainted(request, possible_routed_param) # $ tainted

View File

@@ -0,0 +1,8 @@
from .models import Foo, Bar
from django.contrib import admin
# Register your models here.
admin.site.register(Foo)
admin.site.register(Bar)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TestappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'testapp'

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.2.8 on 2021-10-27 11:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Foo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('field_not_displayed', models.IntegerField()),
],
),
migrations.CreateModel(
name='Bar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('n', models.IntegerField()),
('foo', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='testapp.foo')),
],
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.2.8 on 2021-10-27 12:06
from django.db import migrations
def add_dummy_data(apps, schema_editor):
Foo = apps.get_model("testapp", "Foo")
Bar = apps.get_model("testapp", "Bar")
f1 = Foo(title="example 1", field_not_displayed=10)
f1.save()
f2 = Foo(title="example 2", field_not_displayed=20)
f2.save()
b1 = Bar(n=42, foo=f1)
b1.save()
b2 = Bar(n=43, foo=f1)
b2.save()
b3 = Bar(n=1000, foo=f2)
b3.save()
class Migration(migrations.Migration):
dependencies = [
('testapp', '0001_initial'),
]
operations = [
migrations.RunPython(add_dummy_data),
]

View File

@@ -0,0 +1,13 @@
from django.db import models
# Create your models here.
class Foo(models.Model):
title = models.CharField(max_length=100)
field_not_displayed = models.IntegerField()
class Bar(models.Model):
n = models.IntegerField()
foo = models.ForeignKey(Foo, on_delete=models.PROTECT)

View File

@@ -0,0 +1,14 @@
from .models import Foo, Bar
from rest_framework import serializers
class FooSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Foo
fields = ["title"]
class BarSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Bar
fields = ["n", "foo"]

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,18 @@
from django.urls import path, include
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r"foos", views.FooViewSet)
router.register(r"bars", views.BarViewSet)
urlpatterns = [
path("", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("class-based-view/", views.MyClass.as_view()), # $routeSetup="lcass-based-view/"
path("function-based-view/", views.function_based_view), # $routeSetup="function-based-view/"
path("cookie-test/", views.cookie_test), # $routeSetup="function-based-view/"
path("exception-test/", views.exception_test), # $routeSetup="exception-test/"
]

View File

@@ -0,0 +1,59 @@
from .models import Foo, Bar
from .serializers import FooSerializer, BarSerializer
from rest_framework import viewsets
from rest_framework.decorators import api_view
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.exceptions import APIException
# Viewsets
# see https://www.django-rest-framework.org/tutorial/quickstart/
class FooViewSet(viewsets.ModelViewSet):
queryset = Foo.objects.all()
serializer_class = FooSerializer
class BarViewSet(viewsets.ModelViewSet):
queryset = Bar.objects.all()
serializer_class = BarSerializer
# class based view
# see https://www.django-rest-framework.org/api-guide/views/#class-based-views
class MyClass(APIView):
def initial(self, request, *args, **kwargs):
# this method will be called before processing any request
super().initial(request, *args, **kwargs)
def get(self, request):
return Response("GET request")
def post(self, request):
return Response("POST request")
# function based view
# see https://www.django-rest-framework.org/api-guide/views/#function-based-views
@api_view(["GET", "POST"])
def function_based_view(request: Request):
return Response({"message": "Hello, world!"})
@api_view(["GET", "POST"])
def cookie_test(request: Request):
resp = Response("wat")
resp.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
resp.set_cookie(key="key4", value="value") # $ CookieWrite CookieName="key" CookieValue="value"
resp.headers["Set-Cookie"] = "key2=value2" # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
resp.cookies["key3"] = "value3" # $ CookieWrite CookieName="key3" CookieValue="value3"
return resp
@api_view(["GET", "POST"])
def exception_test(request: Request):
# see https://www.django-rest-framework.org/api-guide/exceptions/
# note: `code details` not exposed by default
raise APIException("exception details", "code details")

View File

@@ -0,0 +1,16 @@
"""
ASGI config for testproj project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,127 @@
"""
Django settings for testproj project.
Generated by 'django-admin startproject' using Django 3.2.8.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent # $ getAPathArgument=Path(..)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-hqg4$wqk3894#_4p$ibwzpg5+&dvx)%6q45v0yq=-43c886(($'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'testapp.apps.TestappConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'testproj.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'testproj.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@@ -0,0 +1,22 @@
"""testproj URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls), # $ routeSetup="admin/"
path("", include("testapp.urls")), # $routeSetup=""
]

View File

@@ -0,0 +1,16 @@
"""
WSGI config for testproj project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
application = get_wsgi_application()