Commit da31752e authored by nano's avatar nano

implemented path traversal checks

parent 07f5d988
Pipeline #67 failed with stage
in 57 seconds
......@@ -27,6 +27,9 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = ''
SERVER_NAME = ''
FIEX_DATA_FOLDER = '/etc/fiex/data'
FIEX_DATA_FOLDER_FILE_PERM_DEFAULT = 755
FIEX_DATA_FOLDER_DIR_PERM_DEFAULT = 644
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
......@@ -85,6 +88,9 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'TEST': {
'NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
}
}
}
......
......@@ -18,7 +18,8 @@
from django.contrib import admin
from django.urls import path
from django.http import HttpResponse
from .models import Exchange, File
from fiexapp.models import Exchange
@admin.register(Exchange)
class ExchangeAdmin(admin.ModelAdmin):
......
# Copyright (c) David Leeuwestein 2018.
#
# Fiex project site: https://anonym-online.net/fiex
# Fiex projet mail: fiex@anonym-online.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class FiexException(Exception):
"""
Fiex base exception
"""
pass
class FiexIOException(FiexException, IOError):
"""
Base exception for fiex related IOErrorss
"""
pass
class FileNotWritableException(FiexIOException):
"""
Raised if a file is not writable
"""
pass
class OperationNotPermittedException(FiexException):
"""
Raised if the operation is not permitted
"""
pass
class IllegalCharactersException(FiexException):
"""
Raised if a parameters contains illegal characters
"""
pass
# Copyright (c) David Leeuwestein 2018.
#
# Fiex project site: https://anonym-online.net/fiex
# Fiex projet mail: fiex@anonym-online.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from django import forms
class UploadFileForm(forms.Form):
path = forms.CharField(max_length=500)
file_field = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
# Copyright (c) David Leeuwestein 2018.
#
# Fiex project site: https://anonym-online.net/fiex
# Fiex projet mail: fiex@anonym-online.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from django.core.files.uploadhandler import FileUploadHandler
class ProgressBarFileUploadHandler(FileUploadHandler):
def new_file(self, field_name, file_name, content_type, content_length, charset=None, content_type_extra=None):
raise NotImplementedError()
def receive_data_chunk(self, raw_data, start):
raise NotImplementedError()
def file_complete(self, file_size):
raise NotImplementedError()
......@@ -17,13 +17,21 @@
import string
import random
import os
from pathlib import Path
from django.db import models
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password, is_password_usable
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from django.conf import settings
from fiexapp.exceptions import FileNotWritableException, OperationNotPermittedException
DEFAULT_TOKEN_LENGTH = 32
def gen_random_token(length=12, chars=string.ascii_lowercase + string.digits):
def gen_random_token(length=DEFAULT_TOKEN_LENGTH, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _i in range(length))
......@@ -33,20 +41,20 @@ def hash_password(password):
class Exchange(models.Model):
"""
An exchange contains files that can be downloaded by anyone knowing the unique token - and if set - the required password
An exchange contains files that can be downloaded by anyone knowing the unique token -
and if set - the required password
Also the upload of files is possible -if allowed
"""
DEFAULT_TOKEN_LENGTH = 12
"""
safety
"""
token = models.CharField('access token', max_length=100, unique=True,
default=gen_random_token(DEFAULT_TOKEN_LENGTH), editable=False)
default=gen_random_token, editable=False)
# TODO securely safe password!
password = models.CharField('optional password', max_length=100, null=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE,related_name="created_by")
permissions = models.ManyToManyField(User, through="PermissionGranted",related_name="PermissionGranted")
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="created_by")
permissions = models.ManyToManyField(User, through="PermissionGranted", related_name="PermissionGranted")
"""
description
......@@ -54,29 +62,150 @@ class Exchange(models.Model):
description = models.CharField('description', max_length=300)
creation_date = models.DateTimeField('date created')
"""
Permissions
"""
def has_permission(self, user, permission):
raise NotImplementedError()
def change_permission(self, user, permission, granted):
raise NotImplementedError()
class File(models.Model):
"""
An instance of the file class represents a file on the webserver.
Each file belongs to exactly on Exchange.
I/O
"""
path = models.CharField('path stored', max_length=200)
description = models.CharField('description of file', max_length=300, null=True)
Exchange = models.ForeignKey(Exchange, on_delete=models.CASCADE)
def exchange_data_dir_path(self):
"""
:returns the absolute path to the data-folder of this exchange
:return: str
"""
assert isinstance(settings.FIEX_DATA_FOLDER, str)
dest_path = Path(settings.FIEX_DATA_FOLDER, self.token)
return str(dest_path.absolute())
def path_from_exchange_data_dir_root(self, path_str: str):
"""
this function should always be called when dealing with paths to prevent directory traversal.
It assures that the path is relative to the root dir of the exchange data dir
: param path: the relative path
:returns the absolute path from the exchange data dir
:return: str
"""
if os.path.isabs(path_str):
raise OperationNotPermittedException('the given path mustn\'t be absolute')
else:
final_path = str(Path(self.exchange_data_dir_path(),path_str))
final_path = os.path.normpath(final_path)
if os.path.commonprefix([final_path,self.exchange_data_dir_path()]) != self.exchange_data_dir_path():
raise OperationNotPermittedException('directory traversal detected')
return final_path
def create_file(self, content: bytes, path: str, override: bool) -> None:
"""
creates a new file with the given content and safes it into the specified path.
The method does not create any not existing dirs
:param content: the content to write
:param path: the path of the file, relative from the exchange root
:param override: must be true if an existing file should be overriden
:return: None
"""
try:
dest_path = self.path_from_exchange_data_dir_root(path)
if override:
if not dest_path.exists():
raise FileNotFoundError('no file at ' + path + ' but override is set to true')
with dest_path.open('wb') as dest_file:
if not dest_file.writable():
raise FileNotWritableException('file at ' + dest_path.absolute() + ' is not writable')
dest_file.write(content)
else:
if dest_path.exists():
raise FileExistsError('path ' + path + ' already exists in ' + str(self))
with dest_path.open('wb') as dest_file:
if not dest_file.writable():
raise FileNotWritableException('file at ' + dest_path.absolute() + ' is not writable')
dest_file.write(content)
assert isinstance(settings.FIEX_DATA_FOLDER_FILE_PERM_DEFAULT, int)
dest_path.chmod(settings.FIEX_DATA_FOLDER_FILE_PERM_DEFAULT)
# TODO exception handling
except IOError as e:
assert isinstance(e, IOError)
raise e
def delete_file(self, path):
try:
assert isinstance(settings.FIEX_DATA_FOLDER, str)
dest_path = self.path_from_exchange_data_dir_root(path)
if dest_path.exists() and dest_path.is_file():
os.remove(dest_path.absolute())
else:
raise FileNotFoundError('file at ' + path + ' does not exist or is not a file')
# TODO better exception handling
except IOError as e:
raise e
def get_file_content(self, path):
raise NotImplementedError()
def create_dir(self, path):
dest_path = Path(self.exchange_data_dir_path(), path)
def delete_dir(self, path):
raise NotImplementedError()
def get_dir_content(self, path):
raise NotImplementedError()
class Permission(models.Model):
"""
A model representing a permission. A user or a group can have a permission to do something on a special exchange.
"""
name = models.CharField('name of the permission',max_length=20)
description = models.CharField('description of the permission',max_length=300)
name = models.CharField('name of the permission', max_length=20, unique=True)
description = models.CharField('description of the permission', max_length=300, unique=True)
WRITE_PERMISSION = None
READ_PERMISSION = None
CHANGE_PERMISSION = None
DELETE_PERMISSION = None
@staticmethod
def create_necessary_permissions():
if Permission.objects.filter(name='WRITE_PERMISSION').count() <=0:
Permission.WRITE_PERMISSION = Permission(name='WRITE_PERMISSION', description='the permission to write')
Permission.WRITE_PERMISSION.save()
else:
Permission.WRITE_PERMISSION = Permission.objects.get(name='WRITE_PERMISSION')
if Permission.objects.filter(name='READ_PERMISSION').count() <= 0:
Permission.READ_PERMISSION = Permission(name='READ_PERMISSION', description='the permission to read')
Permission.READ_PERMISSION.save()
else:
Permission.READ_PERMISSION = Permission.objects.get(name='READ_PERMISSION')
if Permission.objects.filter(name='CHANGE_PERMISSION').count() <= 0:
Permission.CHANGE_PERMISSION = Permission(name='CHANGE_PERMISSION',
description='the permission to change the exchange')
Permission.CHANGE_PERMISSION.save()
else:
Permission.CHANGE_PERMISSION = Permission.objects.get(name='CHANGE_PERMISSION')
if Permission.objects.filter(name='DELETE_PERMISSION').count() <= 0:
Permission.DELETE_PERMISSION = Permission(name='DELETE_PERMISSION',
description='the permission to delete the exchange')
Permission.DELETE_PERMISSION.save()
else:
Permission.DELETE_PERMISSION = Permission.objects.get(name='DELETE_PERMISSION')
class PermissionGranted(models.Model):
......@@ -85,4 +214,4 @@ class PermissionGranted(models.Model):
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
exchange = models.ForeignKey(Exchange, on_delete=models.CASCADE)
date_granted = models.DateField(null=True, default=timezone.now())
date_granted = models.DateField(null=True)
......@@ -16,17 +16,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import re
import os.path
from django.test import TestCase
from fiexapp.models import Exchange,File, gen_random_token, hash_password
from fiexapp.models import Exchange, gen_random_token, hash_password, Permission
from fiexapp.exceptions import OperationNotPermittedException
from django.utils import timezone
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import User
from fiexapp import WRITE_PERMISSION, CHANGE_PERMISSION, EDIT_PERMISSION, DELETE_PERMISSION, VIEW_PERMISSION
# Create your tests here.
class ExchangeTestCase(TestCase):
def setUp(self):
Permission.create_necessary_permissions()
self.test_user = User.objects.create_superuser(username='test',email='test@example.com',password='verysecretpass')
self.exchange_description = "Django Test Instance"
self.exchange_creation_date = timezone.now()
......@@ -60,34 +63,42 @@ class ExchangeTestCase(TestCase):
self.assertTrue(check_password(password,hash))
def test_has_permission(self):
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, VIEW_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, WRITE_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, CHANGE_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, EDIT_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, DELETE_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, WRITE_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, CHANGE_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, EDIT_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, VIEW_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, DELETE_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, Permission.READ_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, Permission.WRITE_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, Permission.CHANGE_PERMISSION))
self.assertTrue(self.test_exchange1.has_permission(self.test_user1, Permission.DELETE_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, Permission.WRITE_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, Permission.CHANGE_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, Permission.READ_PERMISSION))
self.assertFalse(self.test_exchange1.has_permission(self.test_user2, Permission.DELETE_PERMISSION))
def test_grant_permission(self):
self.test_exchange1.change_permission(self.test_user2,VIEW_PERMISSION,True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, VIEW_PERMISSION))
self.test_exchange1.change_permission(self.test_user2,Permission.READ_PERMISSION,True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, Permission.READ_PERMISSION))
self.test_exchange1.change_permission(self.test_user2, Permission.WRITE_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, Permission.WRITE_PERMISSION))
self.test_exchange1.change_permission(self.test_user2, Permission.CHANGE_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, Permission.CHANGE_PERMISSION))
self.test_exchange1.change_permission(self.test_user2, WRITE_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, WRITE_PERMISSION))
self.test_exchange1.change_permission(self.test_user2, Permission.DELETE_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, Permission.DELETE_PERMISSION))
self.test_exchange1.change_permission(self.test_user2, CHANGE_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, CHANGE_PERMISSION))
def test_exchange_data_dir_path(self):
exchange_data_dir_path = self.test_exchange1.exchange_data_dir_path()
self.assertTrue(os.path.commonprefix([settings.FIEX_DATA_FOLDER, exchange_data_dir_path]))
self.test_exchange1.change_permission(self.test_user2, EDIT_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, EDIT_PERMISSION))
def test_path_from_exchange_data_dir_root(self):
self.assertEqual(self.test_exchange1.path_from_exchange_data_dir_root('test/a'),
os.path.join(self.test_exchange1.exchange_data_dir_path(), 'test/a'))
self.test_exchange1.change_permission(self.test_user2, DELETE_PERMISSION, True)
self.assertTrue(self.test_exchange1.has_permission(self.test_user2, DELETE_PERMISSION))
with self.assertRaises(OperationNotPermittedException):
self.test_exchange1.path_from_exchange_data_dir_root('/test/a')
with self.assertRaises(OperationNotPermittedException):
self.test_exchange1.path_from_exchange_data_dir_root('test/../../../a')
class PermissionTestCase(TestCase):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment