mirror of
https://github.com/smittix/intercept.git
synced 2026-07-26 01:38:10 -07:00
Add rate limiting to login endpoint
Introduced Flask-Limiter to restrict login attempts to 5 per minute per IP, enhancing security against brute-force attacks. Updated error handling to display a user-friendly message when the rate limit is exceeded. Minor improvements to the login page, including clearer error messages and display of the user's IP address.
This commit is contained in:
@@ -39,6 +39,8 @@ from utils.constants import (
|
|||||||
QUEUE_MAX_SIZE,
|
QUEUE_MAX_SIZE,
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
from flask_limiter import Limiter
|
||||||
|
from flask_limiter.util import get_remote_address
|
||||||
# Track application start time for uptime calculation
|
# Track application start time for uptime calculation
|
||||||
import time as _time
|
import time as _time
|
||||||
_app_start_time = _time.time()
|
_app_start_time = _time.time()
|
||||||
@@ -48,9 +50,24 @@ logger = logging.getLogger('intercept.database')
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = "signals_intelligence_secret" # Required for flash messages
|
app.secret_key = "signals_intelligence_secret" # Required for flash messages
|
||||||
|
|
||||||
|
# Set up rate limiting
|
||||||
|
limiter = Limiter(
|
||||||
|
key_func=get_remote_address, # Identifies the user by their IP
|
||||||
|
app=app,
|
||||||
|
storage_uri="memory://", # Use RAM memory (change to redis:// etc. for distributed setups)
|
||||||
|
)
|
||||||
|
|
||||||
# Disable Werkzeug debugger PIN (not needed for local development tool)
|
# Disable Werkzeug debugger PIN (not needed for local development tool)
|
||||||
os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
|
os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ERROR HANDLERS
|
||||||
|
# ============================================
|
||||||
|
@app.errorhandler(429)
|
||||||
|
def ratelimit_handler(e):
|
||||||
|
logger.warning(f"Rate limit exceeded for IP: {request.remote_addr}")
|
||||||
|
flash("Too many login attempts. Please wait one minute before trying again.", "error")
|
||||||
|
return render_template('login.html', version=VERSION), 429
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# SECURITY HEADERS
|
# SECURITY HEADERS
|
||||||
@@ -174,6 +191,7 @@ def logout():
|
|||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
@limiter.limit("5 per minute") # Limit to 5 login attempts per minute per IP
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username = request.form.get('username')
|
username = request.form.get('username')
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ dependencies = [
|
|||||||
"skyfield>=1.45",
|
"skyfield>=1.45",
|
||||||
"pyserial>=3.5",
|
"pyserial>=3.5",
|
||||||
"Werkzeug>=3.1.5",
|
"Werkzeug>=3.1.5",
|
||||||
|
"flask-limiter>=2.5.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
+39
-40
@@ -4,62 +4,61 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>iNTERCEPT // Restricted Access</title>
|
<title>iNTERCEPT // Restricted Access</title>
|
||||||
<link
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}" />
|
||||||
rel="stylesheet"
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}" />
|
||||||
href="{{ url_for('static', filename='css/index.css') }}"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="{{ url_for('static', filename='css/login.css') }}"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="landing-overlay">
|
<div class="landing-overlay">
|
||||||
<div class="landing-scanline"></div>
|
<div class="landing-scanline"></div>
|
||||||
|
|
||||||
<div class="landing-content">
|
<div class="landing-content">
|
||||||
<div class="landing-logo">
|
<div class="landing-logo">
|
||||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
|
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
|
||||||
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
|
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
|
||||||
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
|
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
|
||||||
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
|
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
|
||||||
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
|
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
|
||||||
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
|
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
|
||||||
<circle cx="50" cy="22" r="6" fill="#00ff88" class="logo-dot" />
|
<circle cx="50" cy="22" r="6" fill="#00ff88" class="logo-dot" />
|
||||||
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff" />
|
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff" />
|
||||||
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff" />
|
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff" />
|
||||||
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
|
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="landing-title">SECURE LOGIN</h1>
|
<h1 class="landing-title">SECURE LOGIN</h1>
|
||||||
<p class="landing-tagline">// Restricted Terminal Access</p>
|
<p class="landing-tagline">// Restricted Terminal Access</p>
|
||||||
|
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
|
<div class="flash-container">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="flash-error">
|
<div class="flash-error">
|
||||||
<span class="error-prefix">ERROR_SIG:</span>
|
<span class="error-prefix">SIGNAL_ERR:</span>
|
||||||
<span class="error-message">ACCESS_DENIED // {{ message|upper }}</span>
|
<span class="error-message">{{ message|upper }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="text" name="username" placeholder="OPERATOR ID" class="form-input" required autofocus />
|
<input type="text" name="username" placeholder="OPERATOR ID" class="form-input" required autofocus autocomplete="off" />
|
||||||
<input type="password" name="password" placeholder="ENCRYPTION KEY" class="form-input" required />
|
<input type="password" name="password" placeholder="ENCRYPTION KEY" class="form-input" required />
|
||||||
|
|
||||||
<button type="submit" class="landing-enter-btn">
|
<button type="submit" class="landing-enter-btn">
|
||||||
<span class="btn-text">INITIALIZE SESSION</span>
|
<span class="btn-text">INITIALIZE SESSION</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="landing-version">SYSTEM AUTH v{{ version }}</p>
|
<p class="landing-version">SYSTEM AUTH v{{ version }}</p>
|
||||||
|
<p class="landing-tagline" style="font-size: 0.6rem; opacity: 0.4; margin-top: 10px;">
|
||||||
|
Unauthorized access is logged. IP: {{ request.remote_addr }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user