fpbinject 1.6.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fpbinject/__init__.py +10 -0
- fpbinject/__main__.py +9 -0
- fpbinject/app/__init__.py +41 -0
- fpbinject/app/auto_ban.py +365 -0
- fpbinject/app/middleware.py +155 -0
- fpbinject/app/routes/__init__.py +42 -0
- fpbinject/app/routes/connection.py +402 -0
- fpbinject/app/routes/files.py +280 -0
- fpbinject/app/routes/fpb.py +758 -0
- fpbinject/app/routes/logs.py +308 -0
- fpbinject/app/routes/patch.py +233 -0
- fpbinject/app/routes/symbols.py +1521 -0
- fpbinject/app/routes/transfer.py +830 -0
- fpbinject/app/routes/watch.py +174 -0
- fpbinject/app/routes/watch_expr.py +237 -0
- fpbinject/app/utils/__init__.py +0 -0
- fpbinject/app/utils/sse.py +80 -0
- fpbinject/cli/__init__.py +12 -0
- fpbinject/cli/connection_plan.py +67 -0
- fpbinject/cli/discover.py +295 -0
- fpbinject/cli/fpb_cli.py +2242 -0
- fpbinject/cli/handle_cache.py +128 -0
- fpbinject/cli/server_proxy.py +553 -0
- fpbinject/core/__init__.py +2 -0
- fpbinject/core/compile_commands.py +426 -0
- fpbinject/core/compiler.py +738 -0
- fpbinject/core/config_schema.py +500 -0
- fpbinject/core/elf_utils.py +779 -0
- fpbinject/core/file_transfer.py +699 -0
- fpbinject/core/gdb_bridge.py +469 -0
- fpbinject/core/gdb_json_print.py +115 -0
- fpbinject/core/gdb_manager.py +359 -0
- fpbinject/core/gdb_session.py +1281 -0
- fpbinject/core/patch_generator.py +493 -0
- fpbinject/core/serial_protocol.py +1182 -0
- fpbinject/core/state.py +390 -0
- fpbinject/core/watch_evaluator.py +290 -0
- fpbinject/fpb_cli.py +42 -0
- fpbinject/fpb_inject.py +837 -0
- fpbinject/main.py +573 -0
- fpbinject/routes.py +59 -0
- fpbinject/services/__init__.py +16 -0
- fpbinject/services/config_file_watcher.py +0 -0
- fpbinject/services/device_worker.py +306 -0
- fpbinject/services/file_watcher.py +314 -0
- fpbinject/services/file_watcher_manager.py +504 -0
- fpbinject/services/log_recorder.py +124 -0
- fpbinject/services/mdns_advertiser.py +224 -0
- fpbinject/services/timer.py +129 -0
- fpbinject/services/virtual_serial.py +266 -0
- fpbinject/static/css/style.css +1192 -0
- fpbinject/static/css/tutorial.css +397 -0
- fpbinject/static/css/workbench.css +3168 -0
- fpbinject/static/js/app.js +65 -0
- fpbinject/static/js/core/config-schema.js +727 -0
- fpbinject/static/js/core/connection.js +423 -0
- fpbinject/static/js/core/i18n.js +175 -0
- fpbinject/static/js/core/logs.js +200 -0
- fpbinject/static/js/core/slots.js +277 -0
- fpbinject/static/js/core/sse.js +81 -0
- fpbinject/static/js/core/state.js +225 -0
- fpbinject/static/js/core/terminal.js +342 -0
- fpbinject/static/js/core/theme.js +90 -0
- fpbinject/static/js/core/version.js +13 -0
- fpbinject/static/js/features/autoinject.js +549 -0
- fpbinject/static/js/features/config.js +602 -0
- fpbinject/static/js/features/editor.js +499 -0
- fpbinject/static/js/features/elfwatcher.js +163 -0
- fpbinject/static/js/features/filebrowser.js +187 -0
- fpbinject/static/js/features/fpb.js +554 -0
- fpbinject/static/js/features/inline-edit.js +228 -0
- fpbinject/static/js/features/patch.js +639 -0
- fpbinject/static/js/features/quick-commands.js +1779 -0
- fpbinject/static/js/features/symbols.js +1265 -0
- fpbinject/static/js/features/transfer.js +2478 -0
- fpbinject/static/js/features/tutorial.js +1163 -0
- fpbinject/static/js/features/watch.js +815 -0
- fpbinject/static/js/lib/beautify.min.js +4915 -0
- fpbinject/static/js/lib/i18next.min.js +2632 -0
- fpbinject/static/js/locales/en.js +708 -0
- fpbinject/static/js/locales/zh-CN.js +665 -0
- fpbinject/static/js/locales/zh-TW.js +667 -0
- fpbinject/static/js/ui/sash.js +185 -0
- fpbinject/static/js/ui/sidebar.js +268 -0
- fpbinject/templates/base.html +52 -0
- fpbinject/templates/index.html +6 -0
- fpbinject/templates/partials/activitybar.html +67 -0
- fpbinject/templates/partials/editor.html +70 -0
- fpbinject/templates/partials/modals.html +28 -0
- fpbinject/templates/partials/scripts.html +110 -0
- fpbinject/templates/partials/sidebar.html +248 -0
- fpbinject/templates/partials/sidebar_config.html +15 -0
- fpbinject/templates/partials/sidebar_device.html +110 -0
- fpbinject/templates/partials/sidebar_quick_commands.html +269 -0
- fpbinject/templates/partials/sidebar_transfer.html +145 -0
- fpbinject/templates/partials/statusbar.html +30 -0
- fpbinject/templates/partials/terminal.html +38 -0
- fpbinject/templates/partials/titlebar.html +30 -0
- fpbinject/templates/partials/tutorial.html +34 -0
- fpbinject/utils/__init__.py +13 -0
- fpbinject/utils/crc.py +299 -0
- fpbinject/utils/helpers.py +108 -0
- fpbinject/utils/net.py +146 -0
- fpbinject/utils/port_lock.py +169 -0
- fpbinject/utils/serial.py +344 -0
- fpbinject/utils/toolchain.py +48 -0
- fpbinject/version.py +13 -0
- fpbinject-1.6.8.dist-info/METADATA +255 -0
- fpbinject-1.6.8.dist-info/RECORD +113 -0
- fpbinject-1.6.8.dist-info/WHEEL +5 -0
- fpbinject-1.6.8.dist-info/entry_points.txt +3 -0
- fpbinject-1.6.8.dist-info/licenses/LICENSE +21 -0
- fpbinject-1.6.8.dist-info/top_level.txt +1 -0
fpbinject/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""FPBInject — runtime code injection for ARM Cortex-M via the FPB unit.
|
|
2
|
+
|
|
3
|
+
This package bundles the WebServer, CLI, and supporting modules. The physical
|
|
4
|
+
layout lives under ``Tools/WebServer/`` and is mapped to the import name
|
|
5
|
+
``fpbinject`` via setuptools ``package-dir`` (see pyproject.toml).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fpbinject.version import __version__
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
fpbinject/__main__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# MIT License
|
|
4
|
+
# Copyright (c) 2025 - 2026 _VIFEXTech
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
FPBInject WebServer Flask Application Package.
|
|
8
|
+
|
|
9
|
+
This package contains the Flask application factory and route blueprints.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from flask import Flask
|
|
15
|
+
from flask_cors import CORS
|
|
16
|
+
|
|
17
|
+
# Locate the package root (holds templates/ and static/) via importlib.resources
|
|
18
|
+
# so it resolves correctly whether run from source or an installed wheel.
|
|
19
|
+
try:
|
|
20
|
+
from importlib.resources import files as _res_files
|
|
21
|
+
|
|
22
|
+
WEBSERVER_DIR = str(_res_files("fpbinject"))
|
|
23
|
+
except Exception: # pragma: no cover - fallback for odd layouts
|
|
24
|
+
WEBSERVER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_app():
|
|
28
|
+
"""Create and configure the Flask application."""
|
|
29
|
+
app = Flask(
|
|
30
|
+
"fpbinject",
|
|
31
|
+
template_folder=os.path.join(WEBSERVER_DIR, "templates"),
|
|
32
|
+
static_folder=os.path.join(WEBSERVER_DIR, "static"),
|
|
33
|
+
)
|
|
34
|
+
CORS(app)
|
|
35
|
+
|
|
36
|
+
# Import and register routes
|
|
37
|
+
from fpbinject.routes import register_routes
|
|
38
|
+
|
|
39
|
+
register_routes(app)
|
|
40
|
+
|
|
41
|
+
return app
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# MIT License
|
|
4
|
+
# Copyright (c) 2025 - 2026 _VIFEXTech
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Auto-ban engine for FPBInject Web Server.
|
|
8
|
+
|
|
9
|
+
Detects and bans malicious IPs based on:
|
|
10
|
+
1. Known vulnerability scan path fingerprints
|
|
11
|
+
2. Request rate limiting for auth-rejected requests
|
|
12
|
+
|
|
13
|
+
Banned IPs receive tarpit (slow) responses to waste scanner resources.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ipaddress
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Known vulnerability scan path fingerprints
|
|
25
|
+
MALICIOUS_PATH_PATTERNS = [
|
|
26
|
+
# Java middleware
|
|
27
|
+
"/xxl-job-admin",
|
|
28
|
+
"/jmx-console",
|
|
29
|
+
"/invoker/JMXInvokerServlet",
|
|
30
|
+
"/invoker/readonly",
|
|
31
|
+
"/wls-wsat/",
|
|
32
|
+
"/ws_utc/",
|
|
33
|
+
"/console/j_security_check",
|
|
34
|
+
"/j_acegi_security_check",
|
|
35
|
+
"/axis2/services",
|
|
36
|
+
# Spring Boot
|
|
37
|
+
"/actuator/env",
|
|
38
|
+
"/actuator/health",
|
|
39
|
+
"/actuator/gateway",
|
|
40
|
+
"/application-dev.properties",
|
|
41
|
+
"/application-prod.properties",
|
|
42
|
+
"/application-stage.properties",
|
|
43
|
+
"/application-pre.properties",
|
|
44
|
+
"/application-prd.properties",
|
|
45
|
+
"/application-production.properties",
|
|
46
|
+
"/application-staging.properties",
|
|
47
|
+
"/application-stg.properties",
|
|
48
|
+
"/application-preview.properties",
|
|
49
|
+
# Spring Cloud Config
|
|
50
|
+
"/config/application",
|
|
51
|
+
# PHP frameworks
|
|
52
|
+
"/index.php/index/index/testsql",
|
|
53
|
+
"/thinkphp/index.php",
|
|
54
|
+
"/general/login/index.php",
|
|
55
|
+
# Monitoring systems
|
|
56
|
+
"/zabbix/",
|
|
57
|
+
"/zabbix/setup.php",
|
|
58
|
+
"/setup.php",
|
|
59
|
+
"/jsrpc.php",
|
|
60
|
+
"/solr/admin/cores",
|
|
61
|
+
"/remote_agent.php",
|
|
62
|
+
# CMS / admin panels
|
|
63
|
+
"/wp/v2/posts",
|
|
64
|
+
"/xmlrpc.php",
|
|
65
|
+
"/resin-admin/",
|
|
66
|
+
"/manager/html",
|
|
67
|
+
# Gateways / APIs
|
|
68
|
+
"/apisix/batch-requests",
|
|
69
|
+
"/apisix/admin/",
|
|
70
|
+
"/kong/status",
|
|
71
|
+
# Path traversal
|
|
72
|
+
"/../",
|
|
73
|
+
"/..;/",
|
|
74
|
+
"../../../../",
|
|
75
|
+
"/etc/passwd",
|
|
76
|
+
"/win.ini",
|
|
77
|
+
# JNDI injection (Log4Shell CVE-2021-44228)
|
|
78
|
+
"${jndi:",
|
|
79
|
+
# OGNL / SpEL expression injection (Struts2, Spring)
|
|
80
|
+
"${(#",
|
|
81
|
+
"${T(",
|
|
82
|
+
"@java.lang.Runtime@",
|
|
83
|
+
"getRuntime().exec(",
|
|
84
|
+
# Netflix Hystrix SpEL injection
|
|
85
|
+
"/hystrix/",
|
|
86
|
+
# Laravel Ignition RCE (CVE-2021-3129)
|
|
87
|
+
"/_ignition/execute-solution",
|
|
88
|
+
# Apache Druid RCE (CVE-2021-25646)
|
|
89
|
+
"/druid/indexer/v1/sampler",
|
|
90
|
+
# MinIO bootstrap
|
|
91
|
+
"/minio/bootstrap/",
|
|
92
|
+
# CGI-BIN traversal
|
|
93
|
+
"/cgi-bin/",
|
|
94
|
+
# Other known vulnerabilities
|
|
95
|
+
"/CFIDE/administrator",
|
|
96
|
+
"/webtools/control/xmlrpc",
|
|
97
|
+
"/webtools/control/SOAPService",
|
|
98
|
+
"/fileserver/",
|
|
99
|
+
"/uddiexplorer/",
|
|
100
|
+
"/ueditor/",
|
|
101
|
+
"/javax.faces.resource/dynamiccontent",
|
|
102
|
+
"/vpn/../vpns/cfg/smb.conf",
|
|
103
|
+
"/openam/",
|
|
104
|
+
"/debug/exec",
|
|
105
|
+
"/tmui/login.jsp",
|
|
106
|
+
"/zentao/",
|
|
107
|
+
"/CTCWebService/",
|
|
108
|
+
"/manage/log/view",
|
|
109
|
+
"/log/view",
|
|
110
|
+
# JBoss MQ HTTP IL deserialization (CVE-2017-7504)
|
|
111
|
+
"/jbossmq-httpil/HTTPServerILServlet",
|
|
112
|
+
# Confluence OGNL injection (CVE-2022-26134)
|
|
113
|
+
"/pages/createpage-entervariables.action",
|
|
114
|
+
# VMware vCenter / ESXi
|
|
115
|
+
"/ui/vropspluginui/",
|
|
116
|
+
"/eam/vib",
|
|
117
|
+
"/analytics/telemetry/ph/",
|
|
118
|
+
# F5 BIG-IP iControl REST RCE (CVE-2022-1388)
|
|
119
|
+
"/mgmt/tm/util/bash",
|
|
120
|
+
# phpMyAdmin
|
|
121
|
+
"/phpMyAdmin/",
|
|
122
|
+
"/phpmyadmin/",
|
|
123
|
+
"/pma/",
|
|
124
|
+
# Apache Ambari
|
|
125
|
+
"/ambari/api/v1/users/",
|
|
126
|
+
# Fortinet FortiGate VPN (CVE-2018-13379)
|
|
127
|
+
"/remote/logincheck",
|
|
128
|
+
# SaltStack API (CVE-2020-11651)
|
|
129
|
+
"/v1/tools/run",
|
|
130
|
+
# Seeyon OA file upload
|
|
131
|
+
"/develop/systparam/softlogo/",
|
|
132
|
+
# DataEase BI
|
|
133
|
+
"/de2api/datasource/",
|
|
134
|
+
# Exchange Autodiscover SSRF (CVE-2021-34473)
|
|
135
|
+
"/autodiscover/autodiscover.json",
|
|
136
|
+
# Apache Airflow
|
|
137
|
+
"/admin/airflow/",
|
|
138
|
+
# MicroStrategy BI
|
|
139
|
+
"/MicroStrategy/servlet/",
|
|
140
|
+
"/servlet/taskProc",
|
|
141
|
+
# Grafana user creation (CVE-2021-43798)
|
|
142
|
+
"/create_user/",
|
|
143
|
+
# Jira information disclosure
|
|
144
|
+
"/secure/ContactAdministrators",
|
|
145
|
+
# JumpServer session leak
|
|
146
|
+
"/api/v1/terminal/sessions/",
|
|
147
|
+
# Nacos console
|
|
148
|
+
"/api/console/api_server",
|
|
149
|
+
# Roundcube Webmail
|
|
150
|
+
"/composer/send_email",
|
|
151
|
+
# GraphQL introspection
|
|
152
|
+
"/graphql",
|
|
153
|
+
# Azkaban scheduler
|
|
154
|
+
"/azkaban",
|
|
155
|
+
# CASA / NetIQ
|
|
156
|
+
"/casa/nodes/thumbprints",
|
|
157
|
+
# iLO / BMC IPMI
|
|
158
|
+
"/rest/v1/AccountService/",
|
|
159
|
+
# VMware vSAN SpEL injection (CVE-2021-21985)
|
|
160
|
+
"/ui/h5-vsan/rest/proxy/",
|
|
161
|
+
# Struts2 JSON plugin
|
|
162
|
+
"/json",
|
|
163
|
+
# GitLab
|
|
164
|
+
"/users/sign_in",
|
|
165
|
+
"/uploads/user",
|
|
166
|
+
# osinstall
|
|
167
|
+
"/osinstall/v1/device/",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class IPRecord:
|
|
173
|
+
"""Behavior record for a single IP."""
|
|
174
|
+
|
|
175
|
+
first_seen: float = 0.0
|
|
176
|
+
hit_count: int = 0
|
|
177
|
+
reject_count: int = 0
|
|
178
|
+
malicious_score: int = 0
|
|
179
|
+
banned_until: float = 0.0
|
|
180
|
+
ban_count: int = 0
|
|
181
|
+
last_seen: float = 0.0
|
|
182
|
+
recent_timestamps: list = field(default_factory=list)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AutoBanEngine:
|
|
186
|
+
"""Automatic IP ban engine based on behavior analysis."""
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
rate_window=10,
|
|
191
|
+
rate_limit=20,
|
|
192
|
+
malicious_threshold=3,
|
|
193
|
+
ban_duration=3600,
|
|
194
|
+
ban_escalation=2.0,
|
|
195
|
+
max_ban_duration=86400,
|
|
196
|
+
whitelist=None,
|
|
197
|
+
tarpit_delay=10.0,
|
|
198
|
+
):
|
|
199
|
+
"""Initialize the auto-ban engine.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
rate_window: Rate detection window in seconds.
|
|
203
|
+
rate_limit: Max rejected requests within window before ban.
|
|
204
|
+
malicious_threshold: Malicious path hits before ban.
|
|
205
|
+
ban_duration: Base ban duration in seconds.
|
|
206
|
+
ban_escalation: Ban duration multiplier per repeat offense.
|
|
207
|
+
max_ban_duration: Maximum ban duration in seconds.
|
|
208
|
+
whitelist: List of trusted IPs or CIDR ranges.
|
|
209
|
+
tarpit_delay: Seconds to delay response for banned IPs.
|
|
210
|
+
"""
|
|
211
|
+
self.rate_window = rate_window
|
|
212
|
+
self.rate_limit = rate_limit
|
|
213
|
+
self.malicious_threshold = malicious_threshold
|
|
214
|
+
self.ban_duration = ban_duration
|
|
215
|
+
self.ban_escalation = ban_escalation
|
|
216
|
+
self.max_ban_duration = max_ban_duration
|
|
217
|
+
self.whitelist = set(whitelist or [])
|
|
218
|
+
self.tarpit_delay = tarpit_delay
|
|
219
|
+
self.records = defaultdict(IPRecord)
|
|
220
|
+
|
|
221
|
+
def is_whitelisted(self, ip):
|
|
222
|
+
"""Check if IP is in the whitelist (exact or CIDR match)."""
|
|
223
|
+
if ip in self.whitelist:
|
|
224
|
+
return True
|
|
225
|
+
try:
|
|
226
|
+
addr = ipaddress.ip_address(ip)
|
|
227
|
+
for w in self.whitelist:
|
|
228
|
+
if "/" in w:
|
|
229
|
+
if addr in ipaddress.ip_network(w, strict=False):
|
|
230
|
+
return True
|
|
231
|
+
except ValueError:
|
|
232
|
+
pass
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def is_malicious_path(self, path):
|
|
236
|
+
"""Check if request path matches known vulnerability scan fingerprints."""
|
|
237
|
+
path_lower = path.lower()
|
|
238
|
+
return any(p.lower() in path_lower for p in MALICIOUS_PATH_PATTERNS)
|
|
239
|
+
|
|
240
|
+
def check_and_record(self, ip, path):
|
|
241
|
+
"""Pre-auth check: only ban status and malicious path detection.
|
|
242
|
+
|
|
243
|
+
This is called BEFORE token verification. It only checks:
|
|
244
|
+
1. Whether the IP is already banned (tarpit)
|
|
245
|
+
2. Whether the path matches known malicious fingerprints
|
|
246
|
+
|
|
247
|
+
Rate limiting is NOT done here — it is handled by record_reject()
|
|
248
|
+
which is called only after token verification fails. This prevents
|
|
249
|
+
legitimate authenticated users from being rate-limited by normal
|
|
250
|
+
frontend polling.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
ip: Client IP address.
|
|
254
|
+
path: Request path.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
dict with keys:
|
|
258
|
+
action: "allow" or "tarpit"
|
|
259
|
+
reason: Human-readable reason string
|
|
260
|
+
ban_remaining: Seconds remaining in ban (0 if not banned)
|
|
261
|
+
"""
|
|
262
|
+
if self.is_whitelisted(ip):
|
|
263
|
+
return {"action": "allow", "reason": "whitelisted", "ban_remaining": 0}
|
|
264
|
+
|
|
265
|
+
now = time.time()
|
|
266
|
+
rec = self.records[ip]
|
|
267
|
+
|
|
268
|
+
if not rec.first_seen:
|
|
269
|
+
rec.first_seen = now
|
|
270
|
+
|
|
271
|
+
rec.last_seen = now
|
|
272
|
+
rec.hit_count += 1
|
|
273
|
+
|
|
274
|
+
# Already banned -> tarpit
|
|
275
|
+
if rec.banned_until > now:
|
|
276
|
+
remaining = rec.banned_until - now
|
|
277
|
+
return {"action": "tarpit", "reason": "banned", "ban_remaining": remaining}
|
|
278
|
+
|
|
279
|
+
# Malicious path detection (ban immediately on threshold)
|
|
280
|
+
if self.is_malicious_path(path):
|
|
281
|
+
rec.malicious_score += 1
|
|
282
|
+
rec.reject_count += 1
|
|
283
|
+
if rec.malicious_score >= self.malicious_threshold:
|
|
284
|
+
self._ban_ip(
|
|
285
|
+
ip,
|
|
286
|
+
rec,
|
|
287
|
+
f"malicious path threshold ({rec.malicious_score})",
|
|
288
|
+
)
|
|
289
|
+
return {
|
|
290
|
+
"action": "tarpit",
|
|
291
|
+
"reason": f"banned: malicious_score={rec.malicious_score}",
|
|
292
|
+
"ban_remaining": rec.banned_until - now,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {"action": "allow", "reason": "passed", "ban_remaining": 0}
|
|
296
|
+
|
|
297
|
+
def record_reject(self, ip, path):
|
|
298
|
+
"""Record an auth rejection (called after token check fails).
|
|
299
|
+
|
|
300
|
+
This feeds the rate limiter without re-running malicious path check.
|
|
301
|
+
"""
|
|
302
|
+
if self.is_whitelisted(ip):
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
now = time.time()
|
|
306
|
+
rec = self.records[ip]
|
|
307
|
+
rec.reject_count += 1
|
|
308
|
+
rec.recent_timestamps.append(now)
|
|
309
|
+
cutoff = now - self.rate_window
|
|
310
|
+
rec.recent_timestamps = [t for t in rec.recent_timestamps if t > cutoff]
|
|
311
|
+
|
|
312
|
+
if len(rec.recent_timestamps) > self.rate_limit:
|
|
313
|
+
self._ban_ip(
|
|
314
|
+
ip,
|
|
315
|
+
rec,
|
|
316
|
+
f"rate limit after reject ({len(rec.recent_timestamps)}/{self.rate_window}s)",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def _ban_ip(self, ip, rec, reason):
|
|
320
|
+
"""Ban an IP with escalating duration."""
|
|
321
|
+
rec.ban_count += 1
|
|
322
|
+
duration = min(
|
|
323
|
+
self.ban_duration * (self.ban_escalation ** (rec.ban_count - 1)),
|
|
324
|
+
self.max_ban_duration,
|
|
325
|
+
)
|
|
326
|
+
rec.banned_until = time.time() + duration
|
|
327
|
+
logger.warning(
|
|
328
|
+
f"AUTO-BAN: {ip} banned for {duration:.0f}s "
|
|
329
|
+
f"(count={rec.ban_count}, reason={reason}, "
|
|
330
|
+
f"total_hits={rec.hit_count}, rejects={rec.reject_count})"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def get_banned_ips(self):
|
|
334
|
+
"""Get list of currently banned IPs."""
|
|
335
|
+
now = time.time()
|
|
336
|
+
result = []
|
|
337
|
+
for ip, rec in self.records.items():
|
|
338
|
+
if rec.banned_until > now:
|
|
339
|
+
result.append(
|
|
340
|
+
{
|
|
341
|
+
"ip": ip,
|
|
342
|
+
"banned_until": rec.banned_until,
|
|
343
|
+
"remaining": rec.banned_until - now,
|
|
344
|
+
"ban_count": rec.ban_count,
|
|
345
|
+
"total_hits": rec.hit_count,
|
|
346
|
+
"malicious_score": rec.malicious_score,
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
def get_stats(self):
|
|
352
|
+
"""Get engine statistics."""
|
|
353
|
+
now = time.time()
|
|
354
|
+
active_bans = sum(1 for r in self.records.values() if r.banned_until > now)
|
|
355
|
+
return {
|
|
356
|
+
"tracked_ips": len(self.records),
|
|
357
|
+
"active_bans": active_bans,
|
|
358
|
+
"total_bans_issued": sum(r.ban_count for r in self.records.values()),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
def unban_ip(self, ip):
|
|
362
|
+
"""Manually unban an IP."""
|
|
363
|
+
if ip in self.records:
|
|
364
|
+
self.records[ip].banned_until = 0
|
|
365
|
+
logger.info(f"MANUAL-UNBAN: {ip}")
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# MIT License
|
|
4
|
+
# Copyright (c) 2025 - 2026 _VIFEXTech
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Authentication middleware for FPBInject Web Server.
|
|
8
|
+
|
|
9
|
+
Provides token-based authentication for non-localhost access.
|
|
10
|
+
Localhost requests are always allowed without authentication.
|
|
11
|
+
|
|
12
|
+
Includes auto-ban engine that detects and throttles malicious
|
|
13
|
+
vulnerability scanners via path fingerprinting and rate limiting.
|
|
14
|
+
|
|
15
|
+
Security hardening:
|
|
16
|
+
- Constant-time token comparison (prevents timing attacks)
|
|
17
|
+
- Non-blocking tarpit via streaming response (prevents thread exhaustion)
|
|
18
|
+
- CSP and Referrer-Policy headers
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import hmac
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
from flask import request, after_this_request, jsonify, Response
|
|
25
|
+
|
|
26
|
+
from fpbinject.app.auto_ban import AutoBanEngine
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Addresses considered localhost (exempt from auth)
|
|
31
|
+
LOCALHOST_ADDRS = {"127.0.0.1", "::1"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _constant_time_compare(a, b):
|
|
35
|
+
"""Compare two strings in constant time to prevent timing attacks.
|
|
36
|
+
|
|
37
|
+
Uses hmac.compare_digest which is designed to prevent timing
|
|
38
|
+
side-channel attacks on token/password comparison.
|
|
39
|
+
"""
|
|
40
|
+
if a is None or b is None:
|
|
41
|
+
return False
|
|
42
|
+
return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _make_tarpit_response(delay):
|
|
46
|
+
"""Create a streaming response that delays without blocking the worker thread.
|
|
47
|
+
|
|
48
|
+
Instead of time.sleep() which blocks the entire Werkzeug worker thread,
|
|
49
|
+
this uses a generator that yields empty chunks with a pause, allowing
|
|
50
|
+
the WSGI server to handle other requests on remaining threads.
|
|
51
|
+
"""
|
|
52
|
+
import time
|
|
53
|
+
|
|
54
|
+
def slow_generator():
|
|
55
|
+
time.sleep(delay)
|
|
56
|
+
yield b""
|
|
57
|
+
|
|
58
|
+
return Response(
|
|
59
|
+
slow_generator(),
|
|
60
|
+
status=403,
|
|
61
|
+
content_type="text/plain",
|
|
62
|
+
headers={"Cache-Control": "no-store"},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def init_auth(app, token):
|
|
67
|
+
"""Register authentication middleware with auto-ban protection.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
app: Flask application instance
|
|
71
|
+
token: The authentication token string
|
|
72
|
+
"""
|
|
73
|
+
# Create auto-ban engine instance
|
|
74
|
+
ban_engine = AutoBanEngine(
|
|
75
|
+
rate_window=10,
|
|
76
|
+
rate_limit=20,
|
|
77
|
+
malicious_threshold=3,
|
|
78
|
+
ban_duration=3600,
|
|
79
|
+
tarpit_delay=10.0,
|
|
80
|
+
whitelist=["127.0.0.1", "::1"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Store engine on app for test access
|
|
84
|
+
app.ban_engine = ban_engine
|
|
85
|
+
|
|
86
|
+
@app.before_request
|
|
87
|
+
def check_token():
|
|
88
|
+
"""Check authentication token for non-localhost requests."""
|
|
89
|
+
# Localhost is always allowed
|
|
90
|
+
if request.remote_addr in LOCALHOST_ADDRS:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Static resources are public (they contain no sensitive data,
|
|
94
|
+
# and the page itself is protected by token auth)
|
|
95
|
+
if request.path.startswith("/static/"):
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# Auto-ban check (before token verification)
|
|
99
|
+
decision = ban_engine.check_and_record(request.remote_addr, request.path)
|
|
100
|
+
if decision["action"] == "tarpit":
|
|
101
|
+
logger.warning(
|
|
102
|
+
f"Tarpit: {request.remote_addr} -> {request.path} "
|
|
103
|
+
f"(remaining: {decision['ban_remaining']:.0f}s)"
|
|
104
|
+
)
|
|
105
|
+
return _make_tarpit_response(ban_engine.tarpit_delay)
|
|
106
|
+
|
|
107
|
+
# Check token from query, header, or cookie
|
|
108
|
+
req_token = (
|
|
109
|
+
request.args.get("token")
|
|
110
|
+
or request.headers.get("X-Auth-Token")
|
|
111
|
+
or request.cookies.get("fpbinject_token")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not _constant_time_compare(req_token, token):
|
|
115
|
+
logger.warning(f"Auth rejected: {request.remote_addr} -> {request.path}")
|
|
116
|
+
# Record rejection for rate limiting
|
|
117
|
+
ban_engine.record_reject(request.remote_addr, request.path)
|
|
118
|
+
# Return JSON for API routes so frontend can parse the error
|
|
119
|
+
if request.path.startswith("/api/"):
|
|
120
|
+
response = jsonify({"success": False, "error": "Forbidden"})
|
|
121
|
+
response.status_code = 403
|
|
122
|
+
else:
|
|
123
|
+
response = app.make_response(("Forbidden", 403))
|
|
124
|
+
response.headers["Cache-Control"] = "no-store"
|
|
125
|
+
return response
|
|
126
|
+
|
|
127
|
+
# Set cookie on first successful token auth via query/header
|
|
128
|
+
if not request.cookies.get("fpbinject_token"):
|
|
129
|
+
|
|
130
|
+
@after_this_request
|
|
131
|
+
def set_cookie(response):
|
|
132
|
+
response.set_cookie(
|
|
133
|
+
"fpbinject_token",
|
|
134
|
+
token,
|
|
135
|
+
httponly=True,
|
|
136
|
+
samesite="Lax",
|
|
137
|
+
)
|
|
138
|
+
return response
|
|
139
|
+
|
|
140
|
+
@app.after_request
|
|
141
|
+
def add_security_headers(response):
|
|
142
|
+
"""Add security headers to all responses."""
|
|
143
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
144
|
+
response.headers["X-Frame-Options"] = "SAMEORIGIN"
|
|
145
|
+
response.headers["Referrer-Policy"] = "same-origin"
|
|
146
|
+
response.headers["Content-Security-Policy"] = (
|
|
147
|
+
"default-src 'self'; "
|
|
148
|
+
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
|
149
|
+
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
|
150
|
+
"img-src 'self' data: blob:; "
|
|
151
|
+
"connect-src 'self'; "
|
|
152
|
+
"font-src 'self' https://cdn.jsdelivr.net; "
|
|
153
|
+
"worker-src 'self' blob: https://cdn.jsdelivr.net"
|
|
154
|
+
)
|
|
155
|
+
return response
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# MIT License
|
|
4
|
+
# Copyright (c) 2025 - 2026 _VIFEXTech
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Flask API Routes Package.
|
|
8
|
+
|
|
9
|
+
This package contains all API route blueprints organized by functionality.
|
|
10
|
+
During migration, routes are gradually moved from the legacy routes.py module.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from flask import Flask
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_blueprints(app: Flask):
|
|
17
|
+
"""Register all route blueprints with the Flask app."""
|
|
18
|
+
from . import (
|
|
19
|
+
connection,
|
|
20
|
+
files,
|
|
21
|
+
fpb,
|
|
22
|
+
logs,
|
|
23
|
+
patch,
|
|
24
|
+
symbols,
|
|
25
|
+
transfer,
|
|
26
|
+
watch,
|
|
27
|
+
watch_expr,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Register blueprints with /api prefix
|
|
31
|
+
app.register_blueprint(connection.bp, url_prefix="/api")
|
|
32
|
+
app.register_blueprint(fpb.bp, url_prefix="/api")
|
|
33
|
+
app.register_blueprint(logs.bp, url_prefix="/api")
|
|
34
|
+
app.register_blueprint(files.bp, url_prefix="/api")
|
|
35
|
+
app.register_blueprint(watch.bp, url_prefix="/api")
|
|
36
|
+
app.register_blueprint(patch.bp, url_prefix="/api")
|
|
37
|
+
app.register_blueprint(symbols.bp, url_prefix="/api")
|
|
38
|
+
app.register_blueprint(transfer.bp, url_prefix="/api")
|
|
39
|
+
app.register_blueprint(watch_expr.bp, url_prefix="/api")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["register_blueprints"]
|