Skip to content
Snippets Groups Projects
Commit c274d7a5 authored by Alexa Valentová's avatar Alexa Valentová
Browse files

scan attachments with clamav

parent 27c78516
Branches
No related tags found
2 merge requests!1208Release,!1201Release career template test
Pipeline #20306 passed
......@@ -3,4 +3,4 @@
line_length = 88
multi_line_output = 3
include_trailing_comma = true
known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,gql,httplib2,icalendar,instaloader,markdown,modelcluster,nh3,pirates,pytest,pytz,requests,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,willow,yaml
known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,django_ratelimit,environ,faker,fastjsonschema,gql,httplib2,icalendar,instaloader,markdown,modelcluster,nh3,pirates,pytest,pytz,requests,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,willow,yaml
......@@ -152,6 +152,8 @@ V produkci musí být navíc nastaveno:
| `EMAIL_HOST_USER` | --||-- Username |
| `EMAIL_HOST_PASSWORD` | --||-- Heslo |
| `EMAIL_PORT` | --||-- Port |
| `CLAMD_TCP_ADDR` | ClamAV host (pro skenování virů v nahraných souborech) |
| `CLAMD_TCP_SOCKET` | ClamAV socket |
Různé:
......
......@@ -2,6 +2,7 @@ import os
import tempfile
from django import forms
from django.core.exceptions import ValidationError
from shared.forms import ArticlesPageForm as SharedArticlesPageForm
from shared.forms import JekyllImportForm as SharedJekyllImportForm
......@@ -18,15 +19,53 @@ class MultipleFileField(forms.FileField):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
TOTAL_MAX_FILE_SIZE = 25 * 1024 * 1024 # 25 MB
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
total_size = 0
for file in data:
total_size += file.size
if total_size > self.TOTAL_MAX_FILE_SIZE:
raise ValidationError(
"Celková velikost nahraných souborů je příliš velká."
)
result = [single_file_clean(d, initial) for d in data]
else:
result = [single_file_clean(data, initial)]
return result
# Allowed MIME types
ALLOWED_FILE_TYPES = [
"application/pdf", # PDF
"application/msword", # DOC
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # DOCX
"image/png", # PNG
"application/vnd.oasis.opendocument.text", # ODT
]
def validate_file_type(file):
if file.content_type not in ALLOWED_FILE_TYPES:
raise ValidationError(
f"Chybný formát souboru: {file.content_type}. Povolené jsou pouze PDF, DOC, DOCX, PNG, and ODT."
)
def validate_file_size(file, max_size=10 * 1024 * 1024): # Default: 10 MB
if file.size > max_size:
raise ValidationError(
f"Soubory mohou být max. {max_size / (1024 * 1024)} MB velké."
)
class CareerSubmissionForm(forms.Form):
name = forms.CharField(
min_length=1,
......@@ -74,22 +113,35 @@ class CareerSubmissionForm(forms.Form):
cv_file = forms.FileField(
required=True,
validators=[validate_file_type, validate_file_size],
widget=forms.FileInput(
attrs={"class": "max-w-64 mr-auto overflow-hidden break-words"}
attrs={
"class": "max-w-64 mr-auto overflow-hidden break-words",
"accept": ".pdf,.doc,.docx,.png,.odt",
}
),
)
cover_letter_file = forms.FileField(
required=True,
validators=[validate_file_type, validate_file_size],
widget=forms.FileInput(
attrs={"class": "max-w-64 mr-auto overflow-hidden break-words"}
attrs={
"class": "max-w-64 mr-auto overflow-hidden break-words",
"accept": ".pdf,.doc,.docx,.png,.odt",
}
),
)
other_files = MultipleFileField(
required=False,
validators=[validate_file_type, validate_file_size],
widget=MultipleFileInput(
attrs={"class": "max-w-64 mr-auto overflow-hidden break-words"}
)
attrs={
"class": "max-w-64 mr-auto overflow-hidden break-words",
"accept": ".pdf,.doc,.docx,.png,.odt",
}
),
)
personal_data_agreement = forms.BooleanField(required=True)
......
from io import BytesIO
import clamd
from django.conf import settings
from django.http import HttpResponseForbidden
class ClamAVMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
# If there is no Clamd connection set, don't check files as we are presumably
# in a development environment.
if not settings.CLAMD_TCP_SOCKET or not settings.CLAMD_TCP_ADDR:
return self.get_response(request)
cd = clamd.ClamdNetworkSocket(
host=settings.CLAMD_TCP_ADDR, port=settings.CLAMD_TCP_SOCKET, timeout=120
)
if request.method == "POST" and len(request.FILES) > 0:
for file_ in request.FILES.values():
scan_result = cd.instream(BytesIO(file_.read()))
if scan_result["stream"][0] == "FOUND":
return HttpResponseForbidden(
"Nahraný soubor obsahuje potenciálně škodlivý obsah."
)
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
......@@ -4,16 +4,19 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0138_maincareerpage_content'),
("main", "0138_maincareerpage_content"),
]
operations = [
migrations.AddField(
model_name='maincareerpage',
name='recipient_emails',
field=models.CharField(default='', help_text='Zadej buď jednu adresu, nebo víc, oddělených čárkami.', verbose_name='Příjemci emailů o nových přihláškách'),
model_name="maincareerpage",
name="recipient_emails",
field=models.CharField(
default="",
help_text="Zadej buď jednu adresu, nebo víc, oddělených čárkami.",
verbose_name="Příjemci emailů o nových přihláškách",
),
preserve_default=False,
),
]
from datetime import date, datetime
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.mail import EmailMessage
from django.db import models
from django.shortcuts import render
from django_ratelimit.core import is_ratelimited
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from taggit.models import TaggedItemBase
......@@ -515,6 +517,11 @@ class MainCareerPage(
parent_page_types = ["main.MainCareersPage"]
def serve(self, request):
if is_ratelimited(
request, group="career_submissions", key="ip", rate="2/m", method="POST"
):
raise PermissionDenied("Rate limit exceeded")
form = None
current_time = datetime.now()
......@@ -573,7 +580,7 @@ Při otevírání souborů buďte opatrní, virový sken neproběhl!
for file in form.cleaned_data["other_files"]:
email.attach(file.name, file.read(), file.content_type)
sent_successfully = email.send(fail_silently=True)
sent_successfully = email.send()
if sent_successfully:
messages.add_message(
......@@ -583,13 +590,21 @@ Při otevírání souborů buďte opatrní, virový sken neproběhl!
messages.add_message(
request,
messages.ERROR,
"Chyba serveru při odesílání přihlášky.",
"Odeslání přihlášky selhalo. Zkuste to znovu.",
)
else:
errors = ""
for error_val in form.errors.values():
errors += f"{error_val.as_text()}\n"
messages.add_message(
request,
messages.ERROR,
"Chyba při odeslání přihlášky - prohlížeč odeslal chybná data.",
f"""
Odeslání přihlášky selhalo:
{errors}
""",
)
else:
form = CareerSubmissionForm()
......
......@@ -86,7 +86,7 @@
{{ form.cv_file }}
<small class="text-grey-300">(Povinné)</small>
<small class="text-grey-300">(Povinné, max. 10 MB)</small>
</section>
<section class="flex flex-col gap-3 lg:items-center lg:flex-row">
<label
......@@ -97,7 +97,7 @@
{{ form.cover_letter_file }}
<small class="text-grey-300">(Povinný)</small>
<small class="text-grey-300">(Povinný, max. 10 MB)</small>
</section>
<section class="flex flex-col gap-3 lg:items-center lg:flex-row">
<label
......@@ -106,7 +106,13 @@
for="id_other_files"
>Ostatní soubory: </label>
<div class="flex flex-col gap-2">
{{ form.other_files }}
<small class="text-grey-300">
(Max. 10 MB na jeden soubor, 25 MB celkem)
</small>
</div>
</section>
<section class="flex flex-row gap-3 items-start leading-none">
......
......@@ -115,6 +115,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
"main.middlewares.ClamAVMiddleware",
]
# STATIC
......@@ -163,6 +164,12 @@ CSRF_COOKIE_HTTPONLY = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = "DENY"
# ClamAV
CLAMD_USE_TCP = True
CLAMD_TCP_SOCKET = env.int("CLAMD_TCP_SOCKET", default=0)
CLAMD_TCP_ADDR = env.str("CLAMD_TCP_ADDR", default="")
# needed for editing large map collections
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
......
clamd
wagtail
wagtail-metadata
wagtail-trash
......@@ -7,6 +8,7 @@ django-extensions
django-redis
django-settings-export
django-widget-tweaks
django-ratelimit
django-simple-captcha
gql[all]
numpy
......
......@@ -2,29 +2,29 @@
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile base.in
# pip-compile requirements/base.in
#
aiohappyeyeballs==2.4.3
aiohappyeyeballs==2.4.4
# via aiohttp
aiohttp==3.10.10
aiohttp==3.11.10
# via gql
aiosignal==1.3.1
# via aiohttp
amqp==5.2.0
amqp==5.3.1
# via kombu
anyascii==0.3.2
# via wagtail
anyio==4.6.2.post1
anyio==4.7.0
# via
# gql
# httpx
arrow==1.3.0
# via
# -r base.in
# -r requirements/base.in
# ics
asgiref==3.8.1
# via django
asttokens==2.4.1
asttokens==3.0.0
# via stack-data
attrs==24.2.0
# via
......@@ -36,20 +36,20 @@ backoff==2.2.1
# via gql
beautifulsoup4==4.12.3
# via
# -r base.in
# -r requirements/base.in
# wagtail
billiard==4.2.1
# via celery
bleach==6.1.0
# via -r base.in
botocore==1.35.50
bleach==6.2.0
# via -r requirements/base.in
botocore==1.35.78
# via gql
brotli==1.1.0
# via fonttools
cattrs==24.1.2
# via requests-cache
celery==5.4.0
# via -r base.in
# via -r requirements/base.in
certifi==2024.8.30
# via
# httpcore
......@@ -62,6 +62,8 @@ cffi==1.17.1
# weasyprint
charset-normalizer==3.4.0
# via requests
clamd==1.0.2
# via -r requirements/base.in
click==8.1.7
# via
# celery
......@@ -74,7 +76,7 @@ click-plugins==1.1.1
# via celery
click-repl==0.3.0
# via celery
cryptography==43.0.3
cryptography==44.0.0
# via
# josepy
# mozilla-django-oidc
......@@ -87,7 +89,7 @@ defusedxml==0.7.1
# via willow
django==5.0.7
# via
# -r base.in
# -r requirements/base.in
# django-extensions
# django-filter
# django-modelcluster
......@@ -103,9 +105,9 @@ django==5.0.7
# mozilla-django-oidc
# wagtail
django-environ==0.11.2
# via -r base.in
# via -r requirements/base.in
django-extensions==3.2.3
# via -r base.in
# via -r requirements/base.in
django-filter==24.3
# via wagtail
django-modelcluster==6.3
......@@ -114,18 +116,20 @@ django-permissionedforms==0.1
# via wagtail
django-ranged-response==0.2.0
# via django-simple-captcha
django-ratelimit==4.1.0
# via -r requirements/base.in
django-redis==5.4.0
# via -r base.in
# via -r requirements/base.in
django-settings-export==1.2.1
# via -r base.in
# via -r requirements/base.in
django-simple-captcha==0.6.0
# via -r base.in
django-taggit==5.0.1
# via -r requirements/base.in
django-taggit==6.1.0
# via wagtail
django-treebeard==4.7.1
# via wagtail
django-widget-tweaks==1.5.0
# via -r base.in
# via -r requirements/base.in
djangorestframework==3.15.2
# via wagtail
draftjs-exporter==5.0.0
......@@ -134,43 +138,41 @@ et-xmlfile==2.0.0
# via openpyxl
executing==2.1.0
# via stack-data
fastjsonschema==2.20.0
# via -r base.in
fastjsonschema==2.21.1
# via -r requirements/base.in
filetype==1.2.0
# via willow
fonttools[woff]==4.54.1
fonttools[woff]==4.55.3
# via weasyprint
frozenlist==1.5.0
# via
# aiohttp
# aiosignal
gql[all]==3.5.0
# via -r base.in
# via -r requirements/base.in
graphql-core==3.2.5
# via gql
h11==0.14.0
# via httpcore
html5lib==1.1
# via weasyprint
httpcore==1.0.6
httpcore==1.0.7
# via httpx
httplib2==0.22.0
# via -r base.in
httpx==0.27.2
# via -r requirements/base.in
httpx==0.28.1
# via gql
icalendar==6.0.1
# via -r base.in
icalendar==6.1.0
# via -r requirements/base.in
ics==0.7.2
# via -r base.in
# via -r requirements/base.in
idna==3.10
# via
# anyio
# httpx
# requests
# yarl
ipython==8.29.0
# via -r base.in
jedi==0.19.1
ipython==8.30.0
# via -r requirements/base.in
jedi==0.19.2
# via ipython
jmespath==1.0.1
# via botocore
......@@ -183,7 +185,7 @@ l18n==2021.3
laces==0.1.1
# via wagtail
markdown==3.7
# via -r base.in
# via -r requirements/base.in
matplotlib-inline==0.1.7
# via ipython
mozilla-django-oidc==3.0.0
......@@ -192,44 +194,46 @@ multidict==6.1.0
# via
# aiohttp
# yarl
nh3==0.2.18
# via -r base.in
numpy==2.1.2
nh3==0.2.19
# via -r requirements/base.in
numpy==2.2.0
# via
# -r base.in
# -r requirements/base.in
# opencv-python
oauthlib==3.2.2
# via
# requests-oauthlib
# tweepy
opencv-python==4.10.0.84
# via -r base.in
# via -r requirements/base.in
openpyxl==3.1.5
# via wagtail
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pillow==10.4.0
pillow==11.0.0
# via
# django-simple-captcha
# pillow-heif
# wagtail
# weasyprint
pillow-heif==0.20.0
pillow-heif==0.21.0
# via willow
pirates==0.7.0
# via -r base.in
# via -r requirements/base.in
platformdirs==4.3.6
# via requests-cache
prompt-toolkit==3.0.48
# via
# click-repl
# ipython
propcache==0.2.0
# via yarl
propcache==0.2.1
# via
# aiohttp
# yarl
psycopg2-binary==2.9.10
# via -r base.in
# via -r requirements/base.in
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.3
......@@ -240,12 +244,12 @@ pydyf==0.11.0
# via weasyprint
pygments==2.18.0
# via ipython
pyopenssl==24.2.1
pyopenssl==24.3.0
# via josepy
pyparsing==3.2.0
# via httplib2
pypdf2==3.0.1
# via -r base.in
# via -r requirements/base.in
pyphen==0.17.0
# via weasyprint
python-dateutil==2.9.0.post0
......@@ -257,16 +261,16 @@ python-dateutil==2.9.0.post0
# ics
pytz==2024.2
# via
# -r base.in
# -r requirements/base.in
# django-modelcluster
# l18n
pyyaml==6.0.2
# via -r base.in
redis==5.2.0
# via -r requirements/base.in
redis==5.2.1
# via django-redis
requests==2.32.3
# via
# -r base.in
# -r requirements/base.in
# gql
# mozilla-django-oidc
# requests-cache
......@@ -275,33 +279,28 @@ requests==2.32.3
# tweepy
# wagtail
requests-cache==1.2.1
# via -r base.in
# via -r requirements/base.in
requests-oauthlib==1.3.1
# via tweepy
requests-toolbelt==1.0.0
# via gql
sentry-sdk==2.17.0
# via -r base.in
six==1.16.0
sentry-sdk==2.19.2
# via -r requirements/base.in
six==1.17.0
# via
# asttokens
# bleach
# html5lib
# ics
# l18n
# python-dateutil
# url-normalize
sniffio==1.3.1
# via
# anyio
# httpx
# via anyio
soupsieve==2.6
# via beautifulsoup4
sqlparse==0.5.1
sqlparse==0.5.3
# via django
stack-data==0.6.3
# via ipython
tatsu==5.12.1
tatsu==5.12.2
# via ics
telepath==0.3.1
# via wagtail
......@@ -309,16 +308,20 @@ tinycss2==1.4.0
# via
# cssselect2
# weasyprint
tinyhtml5==2.0.0
# via weasyprint
traitlets==5.14.3
# via
# ipython
# matplotlib-inline
tweepy==4.14.0
# via -r base.in
types-python-dateutil==2.9.0.20241003
# via -r requirements/base.in
types-python-dateutil==2.9.0.20241206
# via arrow
typing-extensions==4.12.2
# via ipython
# via
# anyio
# ipython
tzdata==2024.2
# via
# celery
......@@ -337,39 +340,39 @@ vine==5.1.0
# amqp
# celery
# kombu
wagtail==6.2.2
wagtail==6.3.1
# via
# -r base.in
# -r requirements/base.in
# wagtail-metadata
# wagtail-modeladmin
# wagtail-trash
wagtail-metadata==5.0.0
# via -r base.in
# via -r requirements/base.in
wagtail-modeladmin==2.1.0
# via wagtail-trash
wagtail-trash==3.0.0
# via -r base.in
# via -r requirements/base.in
wand==0.6.13
# via -r base.in
# via -r requirements/base.in
wcwidth==0.2.13
# via prompt-toolkit
weasyprint==62.3
# via -r base.in
weasyprint==63.1
# via -r requirements/base.in
webencodings==0.5.1
# via
# bleach
# cssselect2
# html5lib
# tinycss2
# tinyhtml5
websockets==11.0.3
# via gql
whitenoise==5.3.0
# via -r base.in
# via -r requirements/base.in
willow[heif]==1.9.0
# via
# wagtail
# willow
yarl==1.17.0
yarl==1.18.3
# via
# aiohttp
# gql
......
......@@ -2,21 +2,21 @@
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile dev.in
# pip-compile requirements/dev.in
#
asgiref==3.8.1
# via django
coverage[toml]==7.6.4
coverage[toml]==7.6.9
# via pytest-cov
django==5.0.7
# via
# -r dev.in
# -r requirements/dev.in
# django-debug-toolbar
django-debug-toolbar==4.4.6
# via -r dev.in
# via -r requirements/dev.in
factory-boy==3.3.1
# via pytest-factoryboy
faker==30.8.1
faker==33.1.0
# via factory-boy
fastdiff==0.3.0
# via snapshottest
......@@ -26,45 +26,45 @@ inflection==0.5.1
# via pytest-factoryboy
iniconfig==2.0.0
# via pytest
packaging==24.1
packaging==24.2
# via
# pytest
# pytest-factoryboy
# pytest-sugar
pluggy==1.5.0
# via pytest
pytest==8.3.3
pytest==8.3.4
# via
# -r dev.in
# -r requirements/dev.in
# pytest-cov
# pytest-django
# pytest-factoryboy
# pytest-freezegun
# pytest-mock
# pytest-sugar
pytest-cov==5.0.0
# via -r dev.in
pytest-cov==6.0.0
# via -r requirements/dev.in
pytest-django==4.9.0
# via -r dev.in
# via -r requirements/dev.in
pytest-factoryboy==2.7.0
# via -r dev.in
# via -r requirements/dev.in
pytest-freezegun==0.4.2
# via -r dev.in
# via -r requirements/dev.in
pytest-mock==3.14.0
# via -r dev.in
# via -r requirements/dev.in
pytest-sugar==1.0.0
# via -r dev.in
# via -r requirements/dev.in
python-dateutil==2.9.0.post0
# via
# faker
# freezegun
six==1.16.0
six==1.17.0
# via
# python-dateutil
# snapshottest
snapshottest==0.6.0
# via -r dev.in
sqlparse==0.5.1
# via -r requirements/dev.in
sqlparse==0.5.3
# via
# django
# django-debug-toolbar
......
......@@ -2,9 +2,9 @@
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile production.in
# pip-compile requirements/production.in
#
gunicorn==23.0.0
# via -r production.in
packaging==24.1
# via -r requirements/production.in
packaging==24.2
# via gunicorn
<ul class="flex flex-col w-full">
{% for message in messages %}
<script>alert("{{ message }}");</script>
<script>alert(`{{ message }}`);</script>
{% comment %}
<li>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment