Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
227 lines (188 loc) · 6.6 KB
/
Copy pathapp.py
File metadata and controls
227 lines (188 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
"""
TaskFlow - Aplicacao de exemplo da disciplina DevSecOps.
ATENCAO: Esta aplicacao contem vulnerabilidades INTRODUZIDAS DE PROPOSITO
para fins didaticos. NUNCA use este codigo como referencia de boas praticas
e NUNCA implante em ambiente de producao ou exposto a internet.
Vulnerabilidades presentes nesta versao (linha de base do curso):
1. SQL Injection no login e na busca de tarefas (Modulo 3 - SAST)
2. Cross-Site Scripting (XSS) armazenado na descricao da tarefa (Modulo 3/4)
3. Segredo de sessao (SECRET_KEY) hardcoded no codigo (Modulo 1/3)
4. Senhas armazenadas em texto puro no banco (Modulo 2/3)
5. Endpoint de debug exposto publicamente (Modulo 2/4)
6. Dependencias com CVEs conhecidas em requirements.txt (Modulo 3 - SCA)
Ao longo dos encontros, cada uma dessas falhas sera identificada por uma
ferramenta especifica da esteira e corrigida em uma versao "fixed" do codigo.
"""
import sqlite3
from flask import Flask, g, redirect, request, session, url_for
# Vulnerabilidade #3: segredo hardcoded no repositorio.
# Uma ferramenta de SAST/secret-scanning (ex: Gitleaks, Semgrep) deve
# sinalizar esta linha como "Hardcoded Secret".
app = Flask(__name__)
app.config["SECRET_KEY"] = "s3gr3d0-super-secreto-nao-mude-nunca"
DATABASE = "taskflow.db"
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
def init_db():
db = get_db()
db.executescript(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
done INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
);
"""
)
db.commit()
cur = db.execute("SELECT COUNT(*) AS total FROM users")
if cur.fetchone()["total"] == 0:
# Vulnerabilidade #4: senha em texto puro, sem hashing.
db.execute(
"INSERT INTO users (username, password) VALUES (?, ?)",
("admin", "admin123"),
)
db.execute(
"INSERT INTO users (username, password) VALUES (?, ?)",
("aluno", "senha123"),
)
db.commit()
@app.route("/")
def index():
if "user_id" not in session:
return redirect(url_for("login"))
return redirect(url_for("tasks"))
@app.route("/login", methods=["GET", "POST"])
def login():
error = None
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
# Vulnerabilidade #1: SQL Injection.
# A query e montada por concatenacao de string em vez de usar
# parametros preparados (placeholders "?").
query = (
"SELECT * FROM users WHERE username = '"
+ username
+ "' AND password = '"
+ password
+ "'"
)
db = get_db()
cur = db.execute(query)
user = cur.fetchone()
if user:
session["user_id"] = user["id"]
session["username"] = user["username"]
return redirect(url_for("tasks"))
error = "Usuario ou senha invalidos."
return f"""
<h1>TaskFlow - Login</h1>
<form method="post">
Usuario: <input type="text" name="username"><br>
Senha: <input type="password" name="password"><br>
<input type="submit" value="Entrar">
</form>
<p style="color:red">{error or ""}</p>
"""
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("login"))
@app.route("/tasks", methods=["GET"])
def tasks():
if "user_id" not in session:
return redirect(url_for("login"))
search = request.args.get("q", "")
db = get_db()
if search:
# Vulnerabilidade #1 (variante): SQL Injection tambem na busca.
query = (
"SELECT * FROM tasks WHERE user_id = "
+ str(session["user_id"])
+ " AND title LIKE '%"
+ search
+ "%'"
)
rows = db.execute(query).fetchall()
else:
rows = db.execute(
"SELECT * FROM tasks WHERE user_id = ?", (session["user_id"],)
).fetchall()
items = ""
for row in rows:
# Vulnerabilidade #2: XSS armazenado. A descricao do usuario e
# inserida direto no HTML, sem escaping (Jinja2 com | safe
# ou f-string manual como aqui tem o mesmo efeito).
items += f"""
<li>
<b>{row['title']}</b> - {row['description']}
{'(feita)' if row['done'] else ''}
</li>
"""
return f"""
<h1>Minhas tarefas ({session['username']})</h1>
<form method="get">
<input type="text" name="q" placeholder="buscar tarefa">
<input type="submit" value="Buscar">
</form>
<ul>{items}</ul>
<a href="{url_for('new_task')}">Nova tarefa</a> |
<a href="{url_for('logout')}">Sair</a>
"""
@app.route("/tasks/new", methods=["GET", "POST"])
def new_task():
if "user_id" not in session:
return redirect(url_for("login"))
if request.method == "POST":
title = request.form["title"]
description = request.form["description"]
db = get_db()
db.execute(
"INSERT INTO tasks (user_id, title, description) VALUES (?, ?, ?)",
(session["user_id"], title, description),
)
db.commit()
return redirect(url_for("tasks"))
return """
<h1>Nova tarefa</h1>
<form method="post">
Titulo: <input type="text" name="title"><br>
Descricao: <textarea name="description"></textarea><br>
<input type="submit" value="Salvar">
</form>
"""
# Vulnerabilidade #5: endpoint de debug/diagnostico exposto sem
# autenticacao, vazando informacoes internas do servidor.
@app.route("/debug/info")
def debug_info():
import platform
import sys
return {
"python_version": sys.version,
"platform": platform.platform(),
"secret_key": app.config["SECRET_KEY"],
}
if __name__ == "__main__":
with app.app_context():
init_db()
# debug=True em producao expoe o Werkzeug debugger interativo
# (execucao remota de codigo) - tambem sera sinalizado pelo SAST.
app.run(host="0.0.0.0", port=5000, debug=True)