-
-
Notifications
You must be signed in to change notification settings - Fork 6
Fix bugs and harden security for todo app #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2137205
99fcf6b
2428773
82820e0
2dc0aa7
b2bdc4c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Auto detect text files and perform LF normalization | ||
| * text=auto |
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,75 @@ | ||
| # Byte-compiled / optimized / DLL files | ||
| __pycache__/ | ||
| *.py[cod] | ||
| *$py.class | ||
|
|
||
| *.pyc | ||
| # C extensions | ||
| *.so | ||
|
|
||
| # Distribution / packaging | ||
| .Python | ||
| env/ | ||
| build/ | ||
| develop-eggs/ | ||
| dist/ | ||
| downloads/ | ||
| eggs/ | ||
| .eggs/ | ||
| lib/ | ||
| lib64/ | ||
| parts/ | ||
| sdist/ | ||
| var/ | ||
| *.egg-info/ | ||
| .installed.cfg | ||
| *.egg | ||
|
|
||
| # PyInstaller | ||
| # Usually these files are written by a python script from a template | ||
| # before PyInstaller builds the exe, so as to inject date/other infos into it. | ||
| *.manifest | ||
| *.spec | ||
|
|
||
| # Installer logs | ||
| pip-log.txt | ||
| pip-delete-this-directory.txt | ||
|
|
||
| # Unit test / coverage reports | ||
| htmlcov/ | ||
| .coverage | ||
| .tox/ | ||
| .nox/ | ||
| .coverage.* | ||
| .cache | ||
| nosetests.xml | ||
| coverage.xml | ||
| *.cover | ||
| *.py,cover | ||
| .hypothesis/ | ||
|
|
||
| # Translations | ||
| *.mo | ||
| *.pot | ||
|
|
||
| # Django stuff: | ||
| *.log | ||
| local_settings.py | ||
| db.sqlite3 | ||
| db.sqlite3-journal | ||
|
|
||
| # Flask stuff: | ||
| instance/ | ||
| .webassets-cache | ||
|
|
||
| # Scrapy stuff: | ||
| .scrapy | ||
|
|
||
| # Sphinx documentation | ||
| docs/_build/ | ||
|
|
||
| # PyBuilder | ||
| target/ | ||
|
|
||
| # Jupyter Notebook | ||
| .venv/* | ||
| __pycache__/app.cpython-311.pyc |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "cSpell.words": [ | ||
| "bootswatch", | ||
| "jsonify", | ||
| "pytest", | ||
| "sqla", | ||
| "sqlalchemy", | ||
| "todos" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "folders": [ | ||
| { | ||
| "path": "." | ||
| } | ||
| ], | ||
| "settings": { | ||
| "cSpell.words": [ | ||
| "sqlalchemy", | ||
| "venv" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,40 +1,36 @@ | ||||||||||||||||||||||||||||||||
| # [flaskToDo✅](https://dogukanurker.com/flasktodo) | ||||||||||||||||||||||||||||||||
| # How to start | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Simple todo app built with Flask. | ||||||||||||||||||||||||||||||||
| <br/> | ||||||||||||||||||||||||||||||||
| [Contact](mailto:dogukanurker@icloud.com)<br/> | ||||||||||||||||||||||||||||||||
| [Dogukan Urker](https://dogukanurker.com) | ||||||||||||||||||||||||||||||||
| ### [Demo Video 📺](https://youtu.be/GmPeXhBiihI) | ||||||||||||||||||||||||||||||||
| | Desktop | Mobile | | ||||||||||||||||||||||||||||||||
| | :--------------------------------: | :-------------------------------: | | ||||||||||||||||||||||||||||||||
| |  |  | | ||||||||||||||||||||||||||||||||
| ## Windows | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ## Requirements 📦 | ||||||||||||||||||||||||||||||||
| ```console | ||||||||||||||||||||||||||||||||
| REM initialization | ||||||||||||||||||||||||||||||||
| python -m venv .venv | ||||||||||||||||||||||||||||||||
| call .venv\Scripts\activate | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| - Flask | ||||||||||||||||||||||||||||||||
| - Flaskwebgui <sub><sup>(if you run desktop.py)</sup></sub> | ||||||||||||||||||||||||||||||||
| pip install -r requirements.txt | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ## Installation ⬇️ | ||||||||||||||||||||||||||||||||
| REM change firewall settings to open port 5000 for incoming TCP trafic (run in powershell as Administrator): | ||||||||||||||||||||||||||||||||
| REM netsh advfirewall firewall add rule name="Open Port 5000 for Flask web server" dir=in action=allow protocol=TCP localport=5000 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| download source code from Github 💾 | ||||||||||||||||||||||||||||||||
| `git clone https://github.com/DogukanUrker/flaskToDo.git` | ||||||||||||||||||||||||||||||||
| set FLASK_APP=app.py | ||||||||||||||||||||||||||||||||
| set FLASK_ENV=development | ||||||||||||||||||||||||||||||||
| REM run in debug mode (auto reload on code change) | ||||||||||||||||||||||||||||||||
| set FLASK_DEBUG=1 | ||||||||||||||||||||||||||||||||
| flask run --host=0.0.0.0 | ||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The latest Flask documentation (versions 3.1.x and 3.2.x) provides clear guidance on the built-in development server, debug mode, exposing on 0.0.0.0, and production deployments. Exposing on 0.0.0.0: The development server defaults to http://127.0.0.1:5000 (localhost only). To make it externally visible (e.g., from other machines on the network), use flask run --host=0.0.0.0 or app.run(host='0.0.0.0'). However, this is only recommended if the debugger is disabled or you trust the network users, as the interactive debugger poses a security risk allowing arbitrary code execution. Examples show "Running on all addresses" when bound to 0.0.0.0. Debug mode: Enable with flask run --debug or app.run(debug=True). It activates the interactive debugger and reloader. The docs explicitly warn: "Do not run the development server or debugger in a production environment" due to security risks (arbitrary code execution via browser, even with PIN protection). Production server: "Do not use the development server when deploying to production. It is intended for use only during local development. It is not designed to be particularly efficient, stable, or secure." Instead, use a dedicated WSGI server such as Gunicorn, Waitress, mod_wsgi, uWSGI, or gevent, optionally with a reverse proxy like nginx or Apache httpd. Hosting platforms like PythonAnywhere, Google App Engine, AWS Elastic Beanstalk are also recommended. Citations:
Don't expose the debug server on 0.0.0.0 in the quickstart. The current instructions combine For a quickstart guide, keep the default setup bound to localhost (the Flask default), and move any network-facing deployment guidance to a separate production section that emphasizes using a proper WSGI server (Gunicorn, Waitress, etc.) instead of the development server. 📝 Suggested doc changesWindows (lines 12–19): -REM change firewall settings to open port 5000 for incoming TCP trafic (run in powershell as Administrator):
-REM netsh advfirewall firewall add rule name="Open Port 5000 for Flask web server" dir=in action=allow protocol=TCP localport=5000
-
set FLASK_APP=app.py
set FLASK_ENV=development
REM run in debug mode (auto reload on code change)
set FLASK_DEBUG=1
-flask run --host=0.0.0.0
+flask runLinux/macOS (lines 31–35): export FLASK_APP=app.py
export FLASK_ENV=development
# run in debug mode (auto reload on code change)
export FLASK_DEBUG=1
-flask run --host=0.0.0.0
+flask run📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| go to directory 📁 | ||||||||||||||||||||||||||||||||
| `cd flaskToDo` | ||||||||||||||||||||||||||||||||
| ## Linux/MacOS | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Apple’s current platform name is 🧰 Tools🪛 LanguageTool[uncategorized] ~22-~22: The operating system from Apple is written “macOS”. (MAC_OS) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| install requirements.txt 🔽 | ||||||||||||||||||||||||||||||||
| `pip install -r requirements.txt` | ||||||||||||||||||||||||||||||||
| ```console | ||||||||||||||||||||||||||||||||
| # initialization | ||||||||||||||||||||||||||||||||
| python -m venv .venv | ||||||||||||||||||||||||||||||||
| source .venv/bin/activate | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| it's ready to run 🎉 | ||||||||||||||||||||||||||||||||
| `python app.py` | ||||||||||||||||||||||||||||||||
| or | ||||||||||||||||||||||||||||||||
| `python desktop.py` | ||||||||||||||||||||||||||||||||
| for run flaskToDo as a desktop app 💯 | ||||||||||||||||||||||||||||||||
| pip install -r requirements.txt | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ### Contributors 💕 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <a href="https://github.com/dogukanurker"><img src="https://avatars.githubusercontent.com/u/62756402" title="ngryman" width="80" height="80"></a> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ### Support 💰 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <a href="https://dogukanurker.com/donate" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/arial-red.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a> | ||||||||||||||||||||||||||||||||
| export FLASK_APP=app.py | ||||||||||||||||||||||||||||||||
| export FLASK_ENV=development | ||||||||||||||||||||||||||||||||
| # run in debug mode (auto reload on code change) | ||||||||||||||||||||||||||||||||
| export FLASK_DEBUG=1 | ||||||||||||||||||||||||||||||||
| flask run --host=0.0.0.0 | ||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,88 +1,66 @@ | ||
| import socket | ||
| import sqlite3 | ||
| import secrets | ||
| import dbChecker | ||
| from datetime import datetime | ||
| from flask import Flask, render_template, redirect | ||
| from flask import Flask, render_template, request, redirect, url_for, flash | ||
| from flask_sqlalchemy import SQLAlchemy | ||
| from flask_wtf.csrf import CSRFProtect | ||
|
|
||
| app = Flask(__name__) | ||
| app.secret_key = secrets.token_urlsafe(32) | ||
| # /// = relative path, //// = absolute path | ||
| app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' | ||
| app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False | ||
| app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production' | ||
|
|
||
| db = SQLAlchemy(app) | ||
| csrf = CSRFProtect(app) | ||
|
Comment on lines
+9
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Flask and Flask-WTF, the SECRET_KEY is used to securely sign the session cookie, preventing tampering and ensuring data integrity in client-side sessions. It also protects CSRF tokens in Flask-WTF by signing them to validate form submissions and prevent cross-site request forgery attacks. Extensions and applications can use it for other security-related signing needs. Committing a hardcoded default SECRET_KEY to source control exposes it publicly, allowing attackers to forge valid session cookies (e.g., impersonate admins), generate valid CSRF tokens to bypass protections, and potentially achieve remote code execution or full server takeover if the app has vulnerable endpoints, as seen in real-world CVEs like CVE-2024-3408. Citations:
Move Committing a hardcoded default key weakens both session signing and CSRF protection. Attackers with access to the repository can forge session cookies and generate valid CSRF tokens, potentially achieving full server takeover if endpoints are vulnerable. Load it from environment variables instead. Suggested change-app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production'
+app.config['SECRET_KEY'] = os.environ['SECRET_KEY']Add 🧰 Tools🪛 Ruff (0.15.6)[error] 9-9: Possible hardcoded password assigned to: "SECRET_KEY" (S105) 🤖 Prompt for AI Agents |
||
|
|
||
| def currentDate(): | ||
| return datetime.now().strftime("%d.%m.%y") | ||
| class Todo(db.Model): | ||
| id = db.Column(db.Integer, primary_key=True) | ||
| title = db.Column(db.String(100)) | ||
| complete = db.Column(db.Boolean) | ||
|
|
||
| with app.app_context(): | ||
| db.create_all() | ||
|
|
||
| def currentTime(): | ||
| return datetime.now().strftime("%H:%M") | ||
|
|
||
|
|
||
| @app.route("/") | ||
| @app.get("/") | ||
| def index(): | ||
| connection = sqlite3.connect("todos.db") | ||
| cursor = connection.cursor() | ||
| cursor.execute("select * from todos") | ||
| todos = cursor.fetchall() | ||
| return render_template("/index.html", todos=todos) | ||
|
|
||
|
|
||
| @app.route("/add/<todo>") | ||
| def add(todo): | ||
| connection = sqlite3.connect("todos.db") | ||
| cursor = connection.cursor() | ||
| cursor.execute( | ||
| f'insert into todos(todo,date,time) values("{todo}","{currentDate()}","{currentTime()}")' | ||
| ) | ||
| connection.commit() | ||
| return redirect("/") | ||
|
|
||
|
|
||
| @app.route("/check/<int:id>") | ||
| def check(id): | ||
| connection = sqlite3.connect("todos.db") | ||
| cursor = connection.cursor() | ||
| cursor.execute(f'update todos set status = "True" where id = {id}') | ||
| cursor.execute(f'update todos set editDate = "{currentDate()}" where id = {id}') | ||
| cursor.execute(f'update todos set editTime = "{currentTime()}" where id = {id}') | ||
| connection.commit() | ||
| return redirect("/") | ||
|
|
||
|
|
||
| @app.route("/uncheck/<int:id>") | ||
| def uncheck(id): | ||
| connection = sqlite3.connect("todos.db") | ||
| cursor = connection.cursor() | ||
| cursor.execute(f'update todos set status = "False" where id = {id}') | ||
| connection.commit() | ||
| return redirect("/") | ||
|
|
||
|
|
||
| @app.route("/edit/<int:id>/<todo>") | ||
| def edit(id, todo): | ||
| connection = sqlite3.connect("todos.db") | ||
| cursor = connection.cursor() | ||
| cursor.execute(f'update todos set todo = "{todo}" where id = {id}') | ||
| connection.commit() | ||
| return redirect("/") | ||
|
|
||
|
|
||
| @app.route("/delete/<int:id>") | ||
| def delete(id): | ||
| connection = sqlite3.connect("todos.db") | ||
| cursor = connection.cursor() | ||
| cursor.execute(f"delete from todos where id = {id}") | ||
| cursor.execute(f"update sqlite_sequence set seq = seq-1") | ||
| connection.commit() | ||
| return redirect("/") | ||
|
|
||
|
|
||
| @app.errorhandler(404) | ||
| def page_not_found(e): | ||
| return render_template("404.html"), 404 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| app.run( | ||
| debug=True, | ||
| host=socket.gethostbyname(socket.gethostname()), | ||
| ) | ||
| return render_template("index.html") | ||
|
|
||
| @app.get("/list") | ||
| def list(): | ||
| todo_list = db.session.query(Todo).all() | ||
| return render_template("list.html", todo_list=todo_list) | ||
|
|
||
| @app.get("/todo/<int:todo_id>") | ||
| def todo(todo_id): | ||
| todo = db.session.query(Todo).filter(Todo.id == todo_id).first() | ||
| if todo is None: | ||
| return f"No Todo item {todo_id} found.", 404 | ||
| return render_template("todo.html", todo=todo) | ||
|
|
||
| @app.post("/add") | ||
| def add(): | ||
| title = request.form.get("title", "").strip() | ||
| if not title: | ||
| flash("Title cannot be empty.", "error") | ||
| return redirect(url_for("list")) | ||
| if len(title) > 100: | ||
| flash("Title must be 100 characters or fewer.", "error") | ||
| return redirect(url_for("list")) | ||
| new_todo = Todo(title=title, complete=False) | ||
| db.session.add(new_todo) | ||
| db.session.commit() | ||
| return redirect(url_for("list")) | ||
|
|
||
|
|
||
| @app.post("/update/<int:todo_id>") | ||
| def update(todo_id): | ||
| todo = db.get_or_404(Todo, todo_id) | ||
| todo.complete = not todo.complete | ||
| db.session.commit() | ||
| return redirect(url_for("list")) | ||
|
|
||
|
|
||
| @app.post("/delete/<int:todo_id>") | ||
| def delete(todo_id): | ||
| todo = db.get_or_404(Todo, todo_id) | ||
| db.session.delete(todo) | ||
| db.session.commit() | ||
| return redirect(url_for("list")) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: DogukanUrker/flaskToDo
Length of output: 1267
🏁 Script executed:
# Check git history for LICENSE file changes git log --oneline LICENSERepository: DogukanUrker/flaskToDo
Length of output: 115
🏁 Script executed:
Repository: DogukanUrker/flaskToDo
Length of output: 1124
🏁 Script executed:
Repository: DogukanUrker/flaskToDo
Length of output: 236
🏁 Script executed:
Repository: DogukanUrker/flaskToDo
Length of output: 2935
Restore the original author's copyright notice.
The LICENSE file has been altered to replace the original copyright holder (Doğukan Mete Ürker) with Hubert Nafalski. Since this repository is owned by the original author and the commit message ("Fix bugs and harden security for todo app") does not indicate a copyright transfer, this change appears unauthorized.
Under the MIT license, making contributions or security fixes does not automatically transfer copyright ownership. The correct approach is to:
Copyright (c) 2023 Doğukan Mete ÜrkerCopyright (c) 2026 Hubert Nafalski(with current year)🤖 Prompt for AI Agents