Security hardening: IDOR fixes, rate limiting, secret key, session cookies
- IDOR: ownership checks on WO approve/reject/done, charter update/complete/ send-contracts/request-insurance, captain-contract PDF, insurance-rider PDF, delete accounting entry, delete fuel entry, update vessel - auth.py: rate limiting (10 req/15min), explicit is_active check - owner.py: role guard on /owner/dashboard - __init__.py: random SECRET_KEY if unset, absolute SQLite path, parameterized SQL (no f-strings), session cookie HTTPONLY+SameSite, 8h session lifetime, db.session.get() replacing deprecated query.get() - api.py: P&L double-count bug fixed (wo_cost was summed twice), Content- Disposition filename quoted, APP_BASE_URL env var replaces hardcoded localhost:5010, create_management_company validates password length and email uniqueness, dead code removed from sync_all_accounting - create_admin.py: removed password from console output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-7
@@ -2,20 +2,39 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import secrets
|
||||
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
mail = Mail()
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'tu-clave-secreta-cambia-esto')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///fleet.db'
|
||||
|
||||
# ── Secret key ───────────────────────────────────────────────────
|
||||
_secret = os.environ.get('SECRET_KEY')
|
||||
if not _secret:
|
||||
_secret = secrets.token_hex(32)
|
||||
print('⚠️ WARNING: SECRET_KEY no configurado — se generó uno aleatorio (sesiones no persisten entre reinicios). Configura SECRET_KEY en .env para producción.')
|
||||
app.config['SECRET_KEY'] = _secret
|
||||
|
||||
# ── Database (absolute path) ─────────────────────────────────────
|
||||
_db_path = os.path.join(BASE_DIR, '..', 'instance', 'fleet.db')
|
||||
_db_path = os.path.abspath(_db_path)
|
||||
os.makedirs(os.path.dirname(_db_path), exist_ok=True)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{_db_path}'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# ── Session cookie hardening ──────────────────────────────────────
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=8)
|
||||
|
||||
# ── Mail ─────────────────────────────────────────────────────────
|
||||
app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.gmail.com')
|
||||
app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587))
|
||||
app.config['MAIL_USE_TLS'] = True
|
||||
@@ -123,7 +142,8 @@ def _run_migrations(db):
|
||||
from sqlalchemy import text
|
||||
mgmt = conn.execute(text("SELECT id FROM companies WHERE type='management' LIMIT 1")).fetchone()
|
||||
if mgmt:
|
||||
conn.execute(text(f"UPDATE vessels SET management_company_id = {mgmt[0]} WHERE management_company_id IS NULL"))
|
||||
conn.execute(text("UPDATE vessels SET management_company_id = :mid WHERE management_company_id IS NULL"),
|
||||
{"mid": mgmt[0]})
|
||||
conn.commit()
|
||||
|
||||
# Mark first admin as super_admin if none exists
|
||||
@@ -133,10 +153,11 @@ def _run_migrations(db):
|
||||
if not sup:
|
||||
first_admin = conn.execute(text("SELECT id FROM users WHERE role='admin' LIMIT 1")).fetchone()
|
||||
if first_admin:
|
||||
conn.execute(text(f"UPDATE users SET is_super_admin=1 WHERE id={first_admin[0]}"))
|
||||
conn.execute(text("UPDATE users SET is_super_admin=1 WHERE id=:uid"),
|
||||
{"uid": first_admin[0]})
|
||||
conn.commit()
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from app.models import User
|
||||
return User.query.get(int(user_id))
|
||||
return db.session.get(User, int(user_id))
|
||||
|
||||
Reference in New Issue
Block a user