wafaHell 0.1.1__tar.gz → 0.2.0__tar.gz
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-0.1.1 → wafahell-0.2.0}/PKG-INFO +4 -4
- {wafahell-0.1.1 → wafahell-0.2.0}/README.md +3 -3
- {wafahell-0.1.1 → wafahell-0.2.0}/pyproject.toml +1 -1
- wafahell-0.2.0/wafaHell/__init__.py +3 -0
- wafahell-0.2.0/wafaHell/logger.py +45 -0
- wafahell-0.2.0/wafaHell/middleware.py +151 -0
- wafahell-0.2.0/wafaHell/model.py +23 -0
- wafahell-0.2.0/wafaHell/rateLimiter.py +21 -0
- wafahell-0.2.0/wafaHell/teste.py +16 -0
- wafahell-0.2.0/wafaHell/utils.py +10 -0
- {wafahell-0.1.1 → wafahell-0.2.0}/wafaHell.egg-info/PKG-INFO +4 -4
- {wafahell-0.1.1 → wafahell-0.2.0}/wafaHell.egg-info/SOURCES.txt +5 -0
- wafahell-0.1.1/wafaHell/__init__.py +0 -3
- wafahell-0.1.1/wafaHell/middleware.py +0 -65
- {wafahell-0.1.1 → wafahell-0.2.0}/LICENSE +0 -0
- {wafahell-0.1.1 → wafahell-0.2.0}/setup.cfg +0 -0
- {wafahell-0.1.1 → wafahell-0.2.0}/wafaHell.egg-info/dependency_links.txt +0 -0
- {wafahell-0.1.1 → wafahell-0.2.0}/wafaHell.egg-info/requires.txt +0 -0
- {wafahell-0.1.1 → wafahell-0.2.0}/wafaHell.egg-info/top_level.txt +0 -0
|
@@ -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
|
```
|
|
@@ -5,14 +5,14 @@ Middleware WAF for Flask, to detect SQLi and XSS.
|
|
|
5
5
|
## Instalation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pip install
|
|
8
|
+
pip install wafahell
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
```python
|
|
13
13
|
from flask import Flask
|
|
14
|
-
from
|
|
14
|
+
from wafahell import WafaHell
|
|
15
15
|
|
|
16
16
|
app = Flask(__name__)
|
|
17
|
-
waf =
|
|
17
|
+
waf = WafaHell(app)
|
|
18
18
|
```
|
|
@@ -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)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from flask import request as req, abort
|
|
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)
|
|
12
|
+
|
|
13
|
+
class WafaHell:
|
|
14
|
+
def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False, block_ip=False, rate_limit=False):
|
|
15
|
+
self.app = app
|
|
16
|
+
self.block_code = block_code
|
|
17
|
+
self.log = log_func or Logger()
|
|
18
|
+
self.monitor_mode = monitor_mode
|
|
19
|
+
self.block_ip = block_ip
|
|
20
|
+
self.rate_limit = rate_limit
|
|
21
|
+
|
|
22
|
+
self.rules = [
|
|
23
|
+
r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
|
|
24
|
+
r"' OR '1'='1",
|
|
25
|
+
r"<script.*?>.*?</script>",
|
|
26
|
+
r"javascript:",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if app is not None:
|
|
30
|
+
self.init_app(app)
|
|
31
|
+
|
|
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
|
+
|
|
51
|
+
@app.before_request
|
|
52
|
+
def waf_check():
|
|
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))
|
|
65
|
+
|
|
66
|
+
def detect_attack(self, data: str) -> bool:
|
|
67
|
+
for pattern in self.rules:
|
|
68
|
+
if re.search(pattern, data, re.IGNORECASE):
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
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
|
|
76
|
+
|
|
77
|
+
for key, value in req.args.items():
|
|
78
|
+
if self.detect_attack(value):
|
|
79
|
+
return True, f"QUERY '{key}'", value
|
|
80
|
+
|
|
81
|
+
for key, value in req.headers.items():
|
|
82
|
+
if self.detect_attack(value):
|
|
83
|
+
return True, f"HEADER '{key}'", value
|
|
84
|
+
|
|
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()
|
|
133
|
+
|
|
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:
|
|
145
|
+
ip = req.remote_addr
|
|
146
|
+
user_agent = req.headers.get("User-Agent", "unknown")
|
|
147
|
+
path = req.path
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
```
|
|
@@ -2,7 +2,12 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
wafaHell/__init__.py
|
|
5
|
+
wafaHell/logger.py
|
|
5
6
|
wafaHell/middleware.py
|
|
7
|
+
wafaHell/model.py
|
|
8
|
+
wafaHell/rateLimiter.py
|
|
9
|
+
wafaHell/teste.py
|
|
10
|
+
wafaHell/utils.py
|
|
6
11
|
wafaHell.egg-info/PKG-INFO
|
|
7
12
|
wafaHell.egg-info/SOURCES.txt
|
|
8
13
|
wafaHell.egg-info/dependency_links.txt
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from flask import request, abort
|
|
3
|
-
from urllib.parse import unquote
|
|
4
|
-
|
|
5
|
-
class WafaHell:
|
|
6
|
-
def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False):
|
|
7
|
-
self.app = app
|
|
8
|
-
self.block_code = block_code
|
|
9
|
-
self.log_func = log_func or (lambda msg: print(f"[WAF] {msg}"))
|
|
10
|
-
self.monitor_mode = monitor_mode
|
|
11
|
-
# Regras básicas
|
|
12
|
-
self.rules = [
|
|
13
|
-
r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
|
|
14
|
-
r"' OR '1'='1",
|
|
15
|
-
r"<script.*?>.*?</script>",
|
|
16
|
-
r"javascript:",
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
if app is not None:
|
|
20
|
-
self.init_app(app)
|
|
21
|
-
|
|
22
|
-
def init_app(self, app):
|
|
23
|
-
@app.before_request
|
|
24
|
-
def waf_check():
|
|
25
|
-
if self.is_malicious(request):
|
|
26
|
-
self.log_attack(request)
|
|
27
|
-
if not self.monitor_mode:
|
|
28
|
-
abort(self.block_code)
|
|
29
|
-
|
|
30
|
-
def detect_attack(self, data: str) -> bool:
|
|
31
|
-
for pattern in self.rules:
|
|
32
|
-
if re.search(pattern, data, re.IGNORECASE):
|
|
33
|
-
return True
|
|
34
|
-
return False
|
|
35
|
-
|
|
36
|
-
def is_malicious(self, req) -> bool:
|
|
37
|
-
# URL + Query Params
|
|
38
|
-
if self.detect_attack(req.url) or any(self.detect_attack(v) for v in req.args.values()):
|
|
39
|
-
return True
|
|
40
|
-
|
|
41
|
-
# Headers
|
|
42
|
-
for _, value in req.headers.items():
|
|
43
|
-
if self.detect_attack(value):
|
|
44
|
-
return True
|
|
45
|
-
|
|
46
|
-
# Body
|
|
47
|
-
if req.data and self.detect_attack(req.data.decode(errors="ignore")):
|
|
48
|
-
return True
|
|
49
|
-
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
def log_attack(self, req):
|
|
53
|
-
ip = req.remote_addr
|
|
54
|
-
user_agent = req.headers.get("User-Agent", "unknown")
|
|
55
|
-
path = req.path
|
|
56
|
-
query = req.query_string.decode(errors="ignore") if req.query_string else ""
|
|
57
|
-
|
|
58
|
-
msg = (
|
|
59
|
-
f"Ataque detectado\n",
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|