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

scan attachments with clamav

parent 27c78516
No related branches found
No related tags found
2 merge requests!1208Release,!1201Release career template test
Pipeline #20306 passed
This commit is part of merge request !1201. Comments created here will be created in the context of that merge request.
......@@ -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