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())"
|
||||
DJANGO_SECRET_KEY=
|
||||
|
||||
# Включить режим отладки (ТОЛЬКО для разработки!): 1 = да, пусто/0 = нет
|
||||
DJANGO_DEBUG=1
|
||||
# Режим отладки: для продакшена строго 0 (пусто). 1 — только локальная разработка!
|
||||
DJANGO_DEBUG=0
|
||||
|
||||
# Разрешённые хосты через запятую (для продакшена: ваш домен)
|
||||
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
|
||||
# Разрешённые хосты через запятую (для продакшена: ваш домен, например teacher.labnethub.ru)
|
||||
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 = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
@@ -49,10 +50,37 @@ MIDDLEWARE = [
|
||||
"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: защита от брутфорса логина ---
|
||||
AXES_FAILURE_LIMIT = 5
|
||||
AXES_COOLOFF_TIME = 1 # часы
|
||||
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"
|
||||
|
||||
@@ -109,6 +137,14 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
STORAGES = {
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
@@ -1,14 +1,48 @@
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from .forms import ConsultationRequestForm
|
||||
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):
|
||||
if request.method == "POST":
|
||||
if not _submit_allowed(request):
|
||||
messages.error(
|
||||
request,
|
||||
"Слишком много заявок за короткое время. Попробуйте через 10 минут.",
|
||||
)
|
||||
return redirect(f"{reverse('consultations:home')}#booking")
|
||||
form = ConsultationRequestForm(request.POST)
|
||||
if form.is_valid():
|
||||
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-axes>=8.3.1
|
||||
python-dotenv>=1.0
|
||||
|
||||
gunicorn>=22.0
|
||||
whitenoise>=6.6
|
||||
|
||||
Reference in New Issue
Block a user