Skip to content

Commit 91029ad

Browse files
authored
Merge pull request #11 from beackers/securityagain
fix that dumb security problem
2 parents 91a0575 + c84ea69 commit 91029ad

File tree

10 files changed

+98
-19
lines changed

10 files changed

+98
-19
lines changed

app.py

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from flask import Flask, render_template, jsonify, request, session, abort, redirect
2+
from werkzeug.security import generate_password_hash, check_password_hash
23
from flask_socketio import SocketIO, send, emit
3-
import secrets, time, socket, hashlib, json, logging, sqlite3
4+
import secrets, time, socket, hashlib, json, logging, sqlite3, functools
45

56

7+
# -------- SETUP -------- #
8+
69
# App + WebSocket
710
app = Flask(__name__.split(".")[0])
811
app.config["SECRET_KEY"] = secrets.token_hex(16)
@@ -16,7 +19,7 @@ def startLogger():
1619
if not log.handlers:
1720
handler = logging.FileHandler("static/app.log", encoding="utf-8")
1821
streamHandler = logging.StreamHandler()
19-
fmt = "{asctime} [{levelname}] -- {message}"
22+
fmt = "{asctime} [{levelname}]: \n{message}"
2023
formatter = logging.Formatter(fmt, style="{")
2124
handler.setFormatter(formatter)
2225
streamHandler.setFormatter(formatter)
@@ -78,33 +81,65 @@ def coloredText(stuff, colorcode):
7881
json.dump(data, file)
7982
log.exception("Error loading config.json")
8083

81-
84+
# Check if logged in decorator
85+
def logged_in(route):
86+
@functools.wraps(route)
87+
def wrapper_logged_in(*args, **kwargs):
88+
# for now just worry about the decorator
89+
with sqlite3.connect("myop.db") as c:
90+
cur = c.cursor()
91+
if session.get("user"):
92+
cur.execute("SELECT * FROM users WHERE callsign=?", (session.get("username")))
93+
if not session.get("username") in cur.fetchall():
94+
log.debug("user redirected to login")
95+
return redirect("/login")
96+
else:
97+
log.debug("user passed")
98+
return route(*args, **kwargs)
99+
else:
100+
log.info("user passed without login")
101+
return route(*args, **kwargs)
102+
return wrapper_logged_in
103+
104+
# csrf checking
105+
def needs_csrf(route):
106+
@functools.wraps(route)
107+
def wrapper_csrf(*args, **kwargs):
108+
if "csrf" in session:
109+
return route(*args, **kwargs)
110+
else:
111+
session["csrf"] = secrets.token_hex(16)
112+
return route(*args, **kwargs)
113+
return wrapper_csrf
114+
115+
116+
# ------------ APP ------------- #
82117

83118
# Misc. routes
84119
@app.route("/")
120+
@logged_in
85121
def main():
86122
with open("static/config.json", "r") as f:
87123
title = json.load(f)["title"]
88124
return render_template("main.html", title=title)
89125

90126
@app.route("/log")
127+
@logged_in
91128
def showlog():
92129
with open("static/app.log", "r") as log:
93130
return log.read().encode("utf-8"), 200
94131

95132

96133
# ======== Control Panel ======== #
97-
# Can add an admin login page at a later time.
98134

99135
@app.route("/control", methods=['GET'])
136+
@logged_in
137+
@needs_csrf
100138
def control():
101-
# check user.logged-in logic, for later
102-
# return redirect("/login"), 301
103-
if "csrf" not in session:
104-
session["csrf"] = secrets.token_hex(16)
105139
return render_template("control.html", csrf=session["csrf"])
106140

107141
@app.route("/controlapi", methods=['GET', 'POST'])
142+
@logged_in
108143
def controlapi():
109144
if request.method == "GET":
110145
with open('static/config.json', "r") as file:
@@ -130,24 +165,52 @@ def controlapi():
130165

131166

132167
# The actual login page
133-
# @app.route("/login")
134-
# def login():
135-
# return render_template("login.html")
168+
# Pro tip: no @logged_in (for obvious reasons)
169+
@app.route("/login", methods=["GET", "POST"])
170+
@needs_csrf
171+
def login():
172+
if request.method == "GET":
173+
return render_template("login.html", username=session.get("username") or "", csrf=session["csrf"])
174+
elif request.method == "POST":
175+
u = request.form
176+
if ("csrf" or "username" or "password")not in u: abort(403)
177+
if session["csrf"] != u["csrf"]: abort(403)
178+
log.debug("login form passed basic qualifications")
179+
with sqlite3.connect("myop.db") as c:
180+
cur = c.cursor()
181+
c.row_factory = sqlite3.Row
182+
cur.execute("SELECT callsign, pwdhash FROM users WHERE (callsign = ?)", (u["username"].lower(),))
183+
row = cur.fetchone()
184+
if row is None:
185+
log.warning(f"someone tried to log in, but username didn't exist\nusername: {u["username"]}")
186+
abort(403)
187+
if (row) and (check_password_hash(row["pwdhash"], u["password"])):
188+
session["user"] = row["callsign"]
189+
log.info("user logged in!")
190+
return redirect("/", code=301)
191+
else:
192+
log.warning(f"someone failed a login!\nusername: {u["username"]}\npassword: {u["password"]}")
193+
abort(403)
194+
elif request.method == "DELETE":
195+
session["user"] = None
196+
return redirect("/login", code=301)
197+
136198

137199

138200

139201
# ======== Bulletins ======== #
140202

141203
@app.route("/bulletins", methods=['GET'])
204+
@logged_in
205+
@needs_csrf
142206
def bulletins():
143207
with open("static/config.json", "r") as f:
144208
config = json.load(f)
145209
if not config.get("services").get("bulletins"): return render_template("disabled.html"), 403
146-
if "csrf" not in session:
147-
session["csrf"] = secrets.token_hex(16)
148210
return render_template("bulletins.html", csrf=session["csrf"], uname="None")
149211

150212
@app.route("/bulletinsapi", methods=["GET"])
213+
@logged_in
151214
def bulletinsapiget():
152215
with sqlite3.connect("myop.db") as c:
153216
cur = c.cursor()
@@ -167,6 +230,7 @@ def bulletinsapiget():
167230
return jsonify(data), 200
168231

169232
@app.route("/bulletinsapi", methods=["POST"])
233+
@logged_in
170234
def bulletinsapipost():
171235
posted = request.form.get("csrf")
172236
stored = session.get("csrf")
@@ -189,6 +253,7 @@ def bulletinsapipost():
189253
return redirect("/bulletins"), 301
190254

191255
@app.route("/bulletinsapi", methods=['DELETE'])
256+
@logged_in
192257
def bulletinsapidelete():
193258
with sqlite3.connect("myop.db") as c:
194259
cur = c.cursor()
@@ -201,6 +266,7 @@ def bulletinsapidelete():
201266
# ======== Chat Stuff ========== #
202267

203268
@app.route("/chat", methods=['GET'])
269+
@logged_in
204270
def chat():
205271
with open("static/config.json", "r") as f:
206272
config = json.load(f)

myop.db

0 Bytes
Binary file not shown.

static/dompurify.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

static/nav.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ function addANav() {
4848

4949
sel.addEventListener("change", () => {
5050
if (sel.value !== current && acceptablePaths.includes(window.location.pathname)) {
51-
window.location.href = sel.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51+
const clean = DOMPurify.sanitize(sel.value);
52+
window.location.href = clean;
5253
}
5354
});
5455

templates/bt.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Don't forget to add in the website URL into the addANav function!!
1515
-->
1616

17+
<script src="{{ url_for('static', filename='dompurify.js') }}"></script>
1718
<script src="{{ url_for('static', filename='nav.js') }}"></script>
1819
<script>
1920
addANav("");

templates/bulletins.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<div id="bulletinList"></div>
3232
</details>
3333

34+
<script src="{{ url_for('static', filename='dompurify.js') }}"></script>
3435
<script src="{{ url_for('static', filename='nav.js') }}"></script>
3536
<script src="{{ url_for('static', filename='bulletins.js') }}"></script>
3637
<script>

templates/control.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<div id="config"></div><br>
1414
<div id="deletebulletins"></div>
1515

16+
<script src="{{ url_for('static', filename='dompurify.js') }}"></script>
1617
<script src="{{ url_for('static', filename='nav.js') }}"></script>
1718
<script src="{{ url_for('static', filename='config.js') }}"></script>
1819
<script>

templates/disabled.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ <h1>Function Disabled</h1>
1616
<p>Sysops chose to disable this function of MyOp. If you need to access it, ask sysop to enable in the control panel.</p><br>
1717
<a href="/">go home</a><br>
1818
<a href="/control">control panel</a>
19+
<script src="{{ url_for('static', filename='dompurify.js') }}"></script>
1920
<script src="{{ url_for('static', filename='nav.js') }}"></script>
2021
<script>
2122
addANav();

templates/login.html

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
<nav>
1010
<div id="navdiv"></div>
1111
</nav>
12-
<!--
13-
Body goes here.
14-
Don't forget to add in the website URL into the addANav function!!
15-
-->
16-
12+
<form action="/login" method="POST">
13+
<label for="username">Call sign:</label>
14+
<input type="text" id="username" name="username" value="{{ username }}"><br>
15+
<label for="password">Password:</label>
16+
<input type="password" id="password" name="password"><br>
17+
<input type="hidden" name="csrf" value="{{ csrf }}">
18+
<input type="submit">
19+
</form>
20+
<script src="{{ url_for('static', filename='dompurify.js') }}"></script>
1721
<script src="{{ url_for('static', filename='nav.js') }}"></script>
1822
<script>
1923
addANav("");

templates/main.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ <h2>{{ title }}</h2>
1818
<li><a href="/control">control panel</a></li>
1919
</ul>
2020

21+
<script src="{{ url_for('static', filename='dompurify.js') }}"></script>
2122
<script src="{{ url_for('static', filename='nav.js') }}"></script>
2223
<script>
2324
addANav("/");

0 commit comments

Comments
 (0)