wafaHell 0.1.1__py3-none-any.whl → 0.2.0__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.
- wafaHell/__init__.py +1 -1
- wafaHell/logger.py +45 -0
- wafaHell/middleware.py +116 -30
- wafaHell/model.py +23 -0
- wafaHell/rateLimiter.py +21 -0
- wafaHell/teste.py +16 -0
- wafaHell/utils.py +10 -0
- {wafahell-0.1.1.dist-info → wafahell-0.2.0.dist-info}/METADATA +4 -4
- wafahell-0.2.0.dist-info/RECORD +12 -0
- wafahell-0.1.1.dist-info/RECORD +0 -7
- {wafahell-0.1.1.dist-info → wafahell-0.2.0.dist-info}/WHEEL +0 -0
- {wafahell-0.1.1.dist-info → wafahell-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {wafahell-0.1.1.dist-info → wafahell-0.2.0.dist-info}/top_level.txt +0 -0
wafaHell/__init__.py
CHANGED
wafaHell/logger.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
class Logger:
|
|
4
|
+
def __init__(self, name="WAF", log_file="waf.log", level=logging.INFO):
|
|
5
|
+
# Cria logger com nome
|
|
6
|
+
self.logger = logging.getLogger(name)
|
|
7
|
+
self.logger.setLevel(level)
|
|
8
|
+
|
|
9
|
+
# Evita handlers duplicados ao reinicializar
|
|
10
|
+
if not self.logger.handlers:
|
|
11
|
+
# Handler para console
|
|
12
|
+
console_handler = logging.StreamHandler()
|
|
13
|
+
console_handler.setLevel(level)
|
|
14
|
+
|
|
15
|
+
# Handler para arquivo
|
|
16
|
+
file_handler = logging.FileHandler(log_file)
|
|
17
|
+
file_handler.setLevel(level)
|
|
18
|
+
|
|
19
|
+
# Formato
|
|
20
|
+
formatter = logging.Formatter(
|
|
21
|
+
"[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
|
|
22
|
+
datefmt="%H:%M:%S - %d/%m/%Y"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console_handler.setFormatter(formatter)
|
|
26
|
+
file_handler.setFormatter(formatter)
|
|
27
|
+
|
|
28
|
+
# Adiciona handlers
|
|
29
|
+
self.logger.addHandler(console_handler)
|
|
30
|
+
self.logger.addHandler(file_handler)
|
|
31
|
+
|
|
32
|
+
def info(self, msg):
|
|
33
|
+
self.logger.info(msg)
|
|
34
|
+
|
|
35
|
+
def warning(self, msg):
|
|
36
|
+
self.logger.warning(msg)
|
|
37
|
+
|
|
38
|
+
def error(self, msg):
|
|
39
|
+
self.logger.error(msg)
|
|
40
|
+
|
|
41
|
+
def critical(self, msg):
|
|
42
|
+
self.logger.critical(msg)
|
|
43
|
+
|
|
44
|
+
def debug(self, msg):
|
|
45
|
+
self.logger.debug(msg)
|
wafaHell/middleware.py
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from flask import request, abort
|
|
2
|
+
from flask import request as req, abort
|
|
3
3
|
from urllib.parse import unquote
|
|
4
|
+
from model import Blocked, get_session
|
|
5
|
+
from logger import Logger
|
|
6
|
+
from utils import is_block_expired
|
|
7
|
+
from rateLimiter import RateLimiter
|
|
8
|
+
from sqlalchemy.exc import OperationalError
|
|
9
|
+
|
|
10
|
+
# Inicializa o RateLimiter
|
|
11
|
+
limiter = RateLimiter(limit=100, window=60)
|
|
4
12
|
|
|
5
13
|
class WafaHell:
|
|
6
|
-
def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False):
|
|
14
|
+
def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False, block_ip=False, rate_limit=False):
|
|
7
15
|
self.app = app
|
|
8
16
|
self.block_code = block_code
|
|
9
|
-
self.
|
|
17
|
+
self.log = log_func or Logger()
|
|
10
18
|
self.monitor_mode = monitor_mode
|
|
11
|
-
|
|
19
|
+
self.block_ip = block_ip
|
|
20
|
+
self.rate_limit = rate_limit
|
|
21
|
+
|
|
12
22
|
self.rules = [
|
|
13
23
|
r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
|
|
14
24
|
r"' OR '1'='1",
|
|
@@ -20,12 +30,38 @@ class WafaHell:
|
|
|
20
30
|
self.init_app(app)
|
|
21
31
|
|
|
22
32
|
def init_app(self, app):
|
|
33
|
+
# Configura a sessão para cada requisição
|
|
34
|
+
@app.before_request
|
|
35
|
+
def create_session():
|
|
36
|
+
try:
|
|
37
|
+
req.session = get_session() # Cria uma nova sessão usando get_session
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.log.error(f"Erro ao criar sessão para requisição: {e}")
|
|
40
|
+
abort(self.block_code) # Retorna erro 500 se não conseguir criar a sessão
|
|
41
|
+
|
|
42
|
+
# Fecha a sessão após cada requisição
|
|
43
|
+
@app.teardown_request
|
|
44
|
+
def close_session(exc=None):
|
|
45
|
+
if hasattr(req, 'session'):
|
|
46
|
+
try:
|
|
47
|
+
req.session.close() # Fecha a sessão para liberar a conexão
|
|
48
|
+
except Exception as e:
|
|
49
|
+
self.log.error(f"Erro ao fechar sessão: {e}")
|
|
50
|
+
|
|
23
51
|
@app.before_request
|
|
24
52
|
def waf_check():
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
self.verify_client_blocked(req)
|
|
54
|
+
self.verify_rate_limit(req)
|
|
55
|
+
is_malicious, attack_local, payload = self.is_malicious(req)
|
|
56
|
+
|
|
57
|
+
if not is_malicious:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if not self.monitor_mode:
|
|
61
|
+
self.log.warning(self.parse_req(req, payload,attack_local))
|
|
62
|
+
self.block_ip_address(req.remote_addr, req.headers.get("User-Agent", "unknown"))
|
|
63
|
+
else:
|
|
64
|
+
self.log.info(self.parse_req(req, payload, attack_local))
|
|
29
65
|
|
|
30
66
|
def detect_attack(self, data: str) -> bool:
|
|
31
67
|
for pattern in self.rules:
|
|
@@ -33,33 +69,83 @@ class WafaHell:
|
|
|
33
69
|
return True
|
|
34
70
|
return False
|
|
35
71
|
|
|
36
|
-
def is_malicious(self, req) -> bool:
|
|
37
|
-
|
|
38
|
-
if self.detect_attack(req.
|
|
39
|
-
return True
|
|
72
|
+
def is_malicious(self, req) -> tuple[bool, str | None, str | None]:
|
|
73
|
+
|
|
74
|
+
if self.detect_attack(req.base_url):
|
|
75
|
+
return True, "URL", req.base_url
|
|
40
76
|
|
|
41
|
-
|
|
42
|
-
for _, value in req.headers.items():
|
|
77
|
+
for key, value in req.args.items():
|
|
43
78
|
if self.detect_attack(value):
|
|
44
|
-
return True
|
|
79
|
+
return True, f"QUERY '{key}'", value
|
|
45
80
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
for key, value in req.headers.items():
|
|
82
|
+
if self.detect_attack(value):
|
|
83
|
+
return True, f"HEADER '{key}'", value
|
|
49
84
|
|
|
50
|
-
|
|
85
|
+
if req.data:
|
|
86
|
+
body_content = req.data.decode(errors="ignore")
|
|
87
|
+
if self.detect_attack(body_content):
|
|
88
|
+
return True, "BODY", body_content
|
|
89
|
+
|
|
90
|
+
if req.is_json:
|
|
91
|
+
json_data = req.get_json(silent=True)
|
|
92
|
+
if json_data:
|
|
93
|
+
import json
|
|
94
|
+
json_str = json.dumps(json_data)
|
|
95
|
+
if self.detect_attack(json_str):
|
|
96
|
+
return True, "JSON BODY", json_str
|
|
97
|
+
|
|
98
|
+
return False, None, None
|
|
99
|
+
|
|
100
|
+
def verify_client_blocked(self, req) -> None:
|
|
101
|
+
session = req.session
|
|
102
|
+
try:
|
|
103
|
+
client_blocked = session.query(Blocked).filter_by(
|
|
104
|
+
ip=req.remote_addr, user_agent=req.headers.get("User-Agent")
|
|
105
|
+
).first()
|
|
106
|
+
if client_blocked:
|
|
107
|
+
if is_block_expired(client_blocked.blocked_at):
|
|
108
|
+
session.delete(client_blocked)
|
|
109
|
+
session.commit()
|
|
110
|
+
self.log.info(f"[UNBLOCKED] IP {req.remote_addr} desbloqueado apos expiracao do bloqueio.")
|
|
111
|
+
else:
|
|
112
|
+
abort(self.block_code)
|
|
113
|
+
except OperationalError as e:
|
|
114
|
+
session.rollback()
|
|
115
|
+
abort(self.block_code)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def block_ip_address(self, ip, user_agent=None):
|
|
119
|
+
if self.block_ip:
|
|
120
|
+
try:
|
|
121
|
+
session = req.session
|
|
122
|
+
blocked_client = Blocked(ip=ip, user_agent=user_agent)
|
|
123
|
+
session.add(blocked_client)
|
|
124
|
+
session.commit()
|
|
125
|
+
self.log.warning(f"[BLOCKED] IP: {ip}, User-Agent: {user_agent}")
|
|
126
|
+
except OperationalError as e:
|
|
127
|
+
self.log.error(f"Erro de banco de dados ao bloquear IP {ip}: {e}")
|
|
128
|
+
session.rollback()
|
|
129
|
+
abort(self.block_code)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self.log.error(f"Erro ao bloquear IP {ip}: {e}")
|
|
132
|
+
session.rollback()
|
|
51
133
|
|
|
52
|
-
def
|
|
134
|
+
def verify_rate_limit(self, req) -> None:
|
|
135
|
+
if self.rate_limit:
|
|
136
|
+
ip = req.remote_addr
|
|
137
|
+
ua = req.headers.get("User-Agent", "unknown")
|
|
138
|
+
if limiter.is_rate_limited(ip, ua):
|
|
139
|
+
self.log.warning(f"[RATE LIMIT] IP: {ip}, User-Agent: {ua} excedeu o limite de requisições.")
|
|
140
|
+
if self.block_ip:
|
|
141
|
+
self.block_ip_address(ip, ua)
|
|
142
|
+
abort(self.block_code)
|
|
143
|
+
|
|
144
|
+
def parse_req(self, req, payload, attack_local=None) -> str:
|
|
53
145
|
ip = req.remote_addr
|
|
54
146
|
user_agent = req.headers.get("User-Agent", "unknown")
|
|
55
147
|
path = req.path
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
msg = (
|
|
59
|
-
|
|
60
|
-
f"IP: {ip}\n"
|
|
61
|
-
f"User-Agent: {user_agent}\n"
|
|
62
|
-
f"Rota: {path}\n"
|
|
63
|
-
f"Query: {unquote(query)}\n"
|
|
64
|
-
)
|
|
65
|
-
self.log_func(msg)
|
|
148
|
+
method = req.method
|
|
149
|
+
attack_local = attack_local or "unknown"
|
|
150
|
+
msg = f"""[ATTACK] IP: {ip}, User-Agent: {user_agent}, Path: {path}, Method: {method}, Payload: {unquote(payload)}, attack_local: {attack_local}"""
|
|
151
|
+
return msg
|
wafaHell/model.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from sqlalchemy import create_engine, Column, Integer, String
|
|
3
|
+
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
|
|
4
|
+
|
|
5
|
+
Base = declarative_base()
|
|
6
|
+
|
|
7
|
+
class Blocked(Base):
|
|
8
|
+
__tablename__ = "blocks"
|
|
9
|
+
|
|
10
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
11
|
+
ip = Column(String, nullable=True)
|
|
12
|
+
user_agent = Column(String, nullable=True)
|
|
13
|
+
blocked_at = Column(String, nullable=False, default=lambda: datetime.now().strftime("%H:%M:%S"))
|
|
14
|
+
|
|
15
|
+
def __repr__(self):
|
|
16
|
+
return f"<Blocked(ip='{self.ip}', user_agent='{self.user_agent}', blocked_at='{self.blocked_at}')>"
|
|
17
|
+
|
|
18
|
+
def get_session(db_url="sqlite:///wafaHell.db"):
|
|
19
|
+
engine = create_engine(db_url, echo=False)
|
|
20
|
+
Base.metadata.create_all(engine)
|
|
21
|
+
session_factory = sessionmaker(bind=engine)
|
|
22
|
+
Session = scoped_session(session_factory)
|
|
23
|
+
return Session
|
wafaHell/rateLimiter.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
from collections import defaultdict, deque
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
class RateLimiter:
|
|
6
|
+
def __init__(self, limit=100, window=60):
|
|
7
|
+
self.limit = limit
|
|
8
|
+
self.window = window
|
|
9
|
+
self.requests_log = defaultdict(lambda: deque())
|
|
10
|
+
|
|
11
|
+
def is_rate_limited(self, ip, ua):
|
|
12
|
+
key = (ip, ua)
|
|
13
|
+
now = datetime.now()
|
|
14
|
+
window_start = now - timedelta(seconds=self.window)
|
|
15
|
+
|
|
16
|
+
while self.requests_log[key] and self.requests_log[key][0] < window_start:
|
|
17
|
+
self.requests_log[key].popleft()
|
|
18
|
+
|
|
19
|
+
self.requests_log[key].append(now)
|
|
20
|
+
|
|
21
|
+
return len(self.requests_log[key]) >= self.limit
|
wafaHell/teste.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from middleware import WafaHell
|
|
2
|
+
from flask import Flask
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
app = Flask(__name__)
|
|
6
|
+
waf = WafaHell(app, monitor_mode=True, block_ip=True, rate_limit=True)
|
|
7
|
+
|
|
8
|
+
# log = logging.getLogger('werkzeug')
|
|
9
|
+
# log.setLevel(logging.ERROR)
|
|
10
|
+
|
|
11
|
+
@app.route('/')
|
|
12
|
+
def home():
|
|
13
|
+
return "Bem-vindo ao site seguro!"
|
|
14
|
+
|
|
15
|
+
if __name__ == '__main__':
|
|
16
|
+
app.run(host='0.0.0.0', port=5000, debug=False)
|
wafaHell/utils.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
def is_block_expired(blocked_time_str: str) -> bool:
|
|
4
|
+
blocked_time = datetime.strptime(blocked_time_str, "%H:%M:%S")
|
|
5
|
+
now = datetime.now()
|
|
6
|
+
blocked_time = blocked_time.replace(year=now.year, month=now.month, day=now.day)
|
|
7
|
+
|
|
8
|
+
diff = now - blocked_time
|
|
9
|
+
return diff >= timedelta(minutes=1)
|
|
10
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wafaHell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Middleware WAF to Flask
|
|
5
5
|
Author-email: Yago Martins <yagomartins30@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -42,14 +42,14 @@ Middleware WAF for Flask, to detect SQLi and XSS.
|
|
|
42
42
|
## Instalation
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
pip install
|
|
45
|
+
pip install wafahell
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
49
49
|
```python
|
|
50
50
|
from flask import Flask
|
|
51
|
-
from
|
|
51
|
+
from wafahell import WafaHell
|
|
52
52
|
|
|
53
53
|
app = Flask(__name__)
|
|
54
|
-
waf =
|
|
54
|
+
waf = WafaHell(app)
|
|
55
55
|
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
wafaHell/__init__.py,sha256=bdafr3LRK7_frr-V6umEJZUqt8RMrOiLJy72DSQfs2o,60
|
|
2
|
+
wafaHell/logger.py,sha256=yMEz5FY6RdNOzQ9RacAiKpfmwtGjnNVha5umeeSotAM,1354
|
|
3
|
+
wafaHell/middleware.py,sha256=nFhfrvoU3yNaS-_yKjAGbP7egdgG9gYwz5pxtFhVmTw,6052
|
|
4
|
+
wafaHell/model.py,sha256=QnnOOgsEvqs0tHWLN6SkxQlnWR6r4KYiutyMxQl1nDo,902
|
|
5
|
+
wafaHell/rateLimiter.py,sha256=_aU3ZORwpH2CCNpRWF44FyVM5slE3QvLyEgqWRcELr4,666
|
|
6
|
+
wafaHell/teste.py,sha256=rnO3lyYQ-d-fJvlX7EQ-qo2T3DkAM6VS_z897Hht77E,399
|
|
7
|
+
wafaHell/utils.py,sha256=htvGKGKY4VLsN8fcDOSQKDYqkTXbq8f3X29MQWq08P0,354
|
|
8
|
+
wafahell-0.2.0.dist-info/licenses/LICENSE,sha256=6bv9v4HamenV3rqm3mhaGOecwGFrgxtVTW7JPfFDmeY,1086
|
|
9
|
+
wafahell-0.2.0.dist-info/METADATA,sha256=v5yAr069OcRN4c3BxogmtQEAA-KKWr96a9ujgjuzbAc,1979
|
|
10
|
+
wafahell-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
wafahell-0.2.0.dist-info/top_level.txt,sha256=VGBo2g3pOeTH2qIXfZDJCSblJgijemMHUHmI0bBgrls,9
|
|
12
|
+
wafahell-0.2.0.dist-info/RECORD,,
|
wafahell-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
wafaHell/__init__.py,sha256=enTIAbStl7o_cDo93-s8nq86BsN3OEGYu9AbtESQVXQ,105
|
|
2
|
-
wafaHell/middleware.py,sha256=UQ5TPcuz6UDyy5FjyUNXSAZqv2IjnOoc3ze2M8He7Tw,2151
|
|
3
|
-
wafahell-0.1.1.dist-info/licenses/LICENSE,sha256=6bv9v4HamenV3rqm3mhaGOecwGFrgxtVTW7JPfFDmeY,1086
|
|
4
|
-
wafahell-0.1.1.dist-info/METADATA,sha256=DKx0AS6ZJ82ATAkBJetN_AG8doVO00xxMI7PwBdcBzQ,1980
|
|
5
|
-
wafahell-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
wafahell-0.1.1.dist-info/top_level.txt,sha256=VGBo2g3pOeTH2qIXfZDJCSblJgijemMHUHmI0bBgrls,9
|
|
7
|
-
wafahell-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|