Compare commits
3
Commits
PRJ-5
...
security-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22102dbb7a | ||
|
|
a01a0da25b | ||
|
|
357099accd |
@@ -0,0 +1,13 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
db.sqlite3
|
||||||
|
staticfiles
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
+8
-4
@@ -5,8 +5,12 @@
|
|||||||
# Сгенерировать: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
# Сгенерировать: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||||
DJANGO_SECRET_KEY=
|
DJANGO_SECRET_KEY=
|
||||||
|
|
||||||
# Включить режим отладки (ТОЛЬКО для разработки!): 1 = да, пусто/0 = нет
|
# Режим отладки: для продакшена строго 0 (пусто). 1 — только локальная разработка!
|
||||||
DJANGO_DEBUG=1
|
DJANGO_DEBUG=0
|
||||||
|
|
||||||
# Разрешённые хосты через запятую (для продакшена: ваш домен)
|
# Разрешённые хосты через запятую (для продакшена: ваш домен, например teacher.labnethub.ru)
|
||||||
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
|
DJANGO_ALLOWED_HOSTS=teacher.labnethub.ru,127.0.0.1,localhost
|
||||||
|
|
||||||
|
# Secure-куки и HSTS (сайт за HTTPS-прокси): держать включёнными (=1)
|
||||||
|
DJANGO_COOKIE_SECURE=1
|
||||||
|
DJANGO_HSTS_SECONDS=31536000
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Python 3.12 slim
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Сначала зависимости — чтобы кэшировать слой
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Код приложения
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Миграции + статика при старте контейнера
|
||||||
|
CMD ["sh", "-c", "python manage.py migrate --noinput && python manage.py collectstatic --noinput && gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 2"]
|
||||||
@@ -40,6 +40,7 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
@@ -49,10 +50,37 @@ MIDDLEWARE = [
|
|||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# --- HTTPS / безопасность кук (сайт работает только через HTTPS-прокси) ---
|
||||||
|
# nginx должен передавать заголовок X-Forwarded-Proto: https
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
SESSION_COOKIE_SECURE = os.environ.get("DJANGO_COOKIE_SECURE", "1") == "1"
|
||||||
|
CSRF_COOKIE_SECURE = os.environ.get("DJANGO_COOKIE_SECURE", "1") == "1"
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SAMESITE = "Lax"
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
|
||||||
|
X_FRAME_OPTIONS = "DENY"
|
||||||
|
SECURE_HSTS_SECONDS = int(os.environ.get("DJANGO_HSTS_SECONDS", "31536000"))
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
# Редирект на HTTPS не включаем — его делает nginx (иначе цикл при неправильном X-Forwarded-Proto)
|
||||||
|
# SECURE_SSL_REDIRECT = True
|
||||||
|
|
||||||
# --- django-axes: защита от брутфорса логина ---
|
# --- django-axes: защита от брутфорса логина ---
|
||||||
AXES_FAILURE_LIMIT = 5
|
AXES_FAILURE_LIMIT = 5
|
||||||
AXES_COOLOFF_TIME = 1 # часы
|
AXES_COOLOFF_TIME = 1 # часы
|
||||||
AXES_RESET_ON_SUCCESS = True
|
AXES_RESET_ON_SUCCESS = True
|
||||||
|
AXES_LOCKOUT_PARAMETERS = ["ip_address"]
|
||||||
|
|
||||||
|
# --- антиспам: лимит заявок с одного IP ---
|
||||||
|
# Кэш in-memory (locmem) подходит для одного воркера; для нескольких воркеров
|
||||||
|
# нужно memcached/redis, иначе лимит считается на каждый процесс отдельно.
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||||
|
"LOCATION": "teacher-site-cache",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = "config.urls"
|
ROOT_URLCONF = "config.urls"
|
||||||
|
|
||||||
@@ -109,6 +137,14 @@ USE_I18N = True
|
|||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||||
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|||||||
@@ -1,14 +1,48 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from .forms import ConsultationRequestForm
|
from .forms import ConsultationRequestForm
|
||||||
from .models import ConsultationSlot
|
from .models import ConsultationSlot
|
||||||
|
|
||||||
|
|
||||||
|
# Антиспам: не более 3 заявок с одного IP за 10 минут
|
||||||
|
SUBMIT_LIMIT = 3
|
||||||
|
SUBMIT_WINDOW_SECONDS = 600
|
||||||
|
|
||||||
|
|
||||||
|
def _client_ip(request):
|
||||||
|
# X-Forwarded-For добавляет nginx; берём первый адрес (реальный клиент)
|
||||||
|
xff = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||||
|
if xff:
|
||||||
|
return xff.split(",")[0].strip()
|
||||||
|
return request.META.get("REMOTE_ADDR", "unknown")
|
||||||
|
|
||||||
|
|
||||||
|
def _submit_allowed(request):
|
||||||
|
key = f"submit:{_client_ip(request)}"
|
||||||
|
now = timezone.now().timestamp()
|
||||||
|
stamps = cache.get(key, [])
|
||||||
|
stamps = [s for s in stamps if now - s < SUBMIT_WINDOW_SECONDS]
|
||||||
|
if len(stamps) >= SUBMIT_LIMIT:
|
||||||
|
cache.set(key, stamps, SUBMIT_WINDOW_SECONDS)
|
||||||
|
return False
|
||||||
|
stamps.append(now)
|
||||||
|
cache.set(key, stamps, SUBMIT_WINDOW_SECONDS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not _submit_allowed(request):
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Слишком много заявок за короткое время. Попробуйте через 10 минут.",
|
||||||
|
)
|
||||||
|
return redirect(f"{reverse('consultations:home')}#booking")
|
||||||
form = ConsultationRequestForm(request.POST)
|
form = ConsultationRequestForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Секреты — только из .env (см. .env.example)
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- sqlite_data:/app/db.sqlite3
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sqlite_data:
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# This is a sample Python script.
|
|
||||||
|
|
||||||
# Press Shift+F10 to execute it or replace it with your code.
|
|
||||||
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
|
|
||||||
|
|
||||||
|
|
||||||
def print_hi(name):
|
|
||||||
# Use a breakpoint in the code line below to debug your script.
|
|
||||||
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
|
|
||||||
|
|
||||||
|
|
||||||
# Press the green button in the gutter to run the script.
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print_hi('PyCharm')
|
|
||||||
|
|
||||||
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
Django>=5.0,<6.0
|
Django>=5.0,<6.0
|
||||||
django-axes>=8.3.1
|
django-axes>=8.3.1
|
||||||
python-dotenv>=1.0
|
python-dotenv>=1.0
|
||||||
|
gunicorn>=22.0
|
||||||
|
whitenoise>=6.6
|
||||||
|
|||||||
Reference in New Issue
Block a user