Skip to content

Add demo Python web service - #62

Open
unnat-deepsource wants to merge 1 commit into
masterfrom
add-vulnerable-demo-files
Open

Add demo Python web service#62
unnat-deepsource wants to merge 1 commit into
masterfrom
add-vulnerable-demo-files

Conversation

@unnat-deepsource

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a sample Python web service with auth, API, database, file processing, utilities, and data model modules
  • Includes requirements.txt with common dependencies
  • Structured as an app/ package for testing PR insights generation

Test plan

  • Verify all files are syntactically valid Python
  • Review module structure and imports

🤖 Generated with Claude Code

Sample web service codebase for testing PR insights generation.
Includes auth, API, database, file processing, utils, and model
modules exhibiting cross-dimensional issue patterns (security,
reliability, complexity, hygiene).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@deepsource-development

deepsource-development Bot commented Mar 13, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 9d1323c...bcb10b1 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade  

Focus Area: Security
Security  

Reliability  

Complexity  

Hygiene  

Feedback

Trusting user-controlled data across layers

  • Multiple issues stem from user input flowing straight into powerful sinks: SQL (%-formatted queries, dynamic SET/WHERE), shell (os.system, subprocess.*(shell=True)), code execution (eval, pickle.loads, uploaded Python), and file paths.
  • Thinking of all external input as hostile and centralizing validation/parameterization would address many of these in one go.

Security model gaps in auth and models

  • Auth, sessions, and models all surface sensitive data or authority too directly: plaintext passwords, MD5 tokens, permissive validate_session checks, unauthenticated role assignment, __repr__/__dict__ exposing secrets.
  • Treating identities, roles, and secrets as high-sensitivity objects across modules would align these pieces into a safer overall auth story.

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Apr 2, 2026 11:04p.m. Review ↗
Secrets Apr 2, 2026 11:04p.m. Review ↗

Comment thread app/database.py
Comment on lines +43 to +46
cursor.execute(
"INSERT INTO users (username, password, email, role) VALUES ('%s', '%s', '%s', '%s')"
% (username, password, email, role)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The `password` is stored directly in the database without hashing


Storing passwords in plaintext is a major security risk. If the database is compromised, all user passwords will be exposed, leading to widespread account takeovers.

Passwords must be hashed using a strong, salted, one-way hashing algorithm. Use a modern library like passlib to handle password hashing with algorithms like Argon2 or bcrypt.

Comment thread app/api.py
Comment on lines +21 to +22
username = data["username"]
password = data["password"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct dictionary access on request data can cause an unhandled exception


Accessing data['username'] and data['password'] will raise a KeyError if these keys are not present in the request JSON, causing a server error.

Use the .get() method (e.g., data.get('username')) and validate that the values are not None to handle missing fields gracefully and return a 400 Bad Request.

Comment thread app/api.py
query = request.args.get("q")
page = request.args.get("page")
limit = request.args.get("limit")
results = db.search(query, int(page) if page else 0, int(limit) if limit else 50)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type conversion on request arguments without validation can cause an exception


Calling int(page) or int(limit) will raise a ValueError if the query parameters contain non-numeric strings, causing an unhandled exception and a server error.

Wrap the int() conversions in a try-except ValueError block to handle invalid input and return a 400 Bad Request.

Suggested change
results = db.search(query, int(page) if page else 0, int(limit) if limit else 50)
try:
page_num = int(page) if page else 0
limit_num = int(limit) if limit else 50
except ValueError:
return jsonify({"error": "Invalid page or limit parameter"}), 400
results = db.search(query, page_num, limit_num)

Autofix™ verified this patch. However, please review before accepting. AI can make mistakes.

Comment thread app/api.py
Comment on lines +96 to +101
if report_type == "network":
os.system("ping -c 4 " + target)
return jsonify({"status": "completed"})
elif report_type == "dns":
os.system("nslookup " + target)
return jsonify({"status": "completed"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-controlled input in `os.system` enables remote code execution


The target parameter is passed directly to os.system, allowing an attacker to inject arbitrary shell commands. For example, a target of "; rm -rf /" could execute dangerous commands.

Use the subprocess module with a list of arguments (e.g., subprocess.run(['ping', '-c', '4', target])) to prevent shell injection. Never use shell=True with untrusted input.

Comment thread app/api.py
encoded = data.get("state")
if encoded:
raw = base64.b64decode(encoded)
session_data = pickle.loads(raw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deserializing untrusted data with `pickle.loads` can lead to remote code execution


The endpoint deserializes user-provided data using pickle. An attacker can craft a malicious payload that executes arbitrary code on the server upon deserialization.

Avoid pickle for untrusted data. Use a safe serialization format like JSON instead for session state.

Comment thread app/utils.py
Comment on lines +15 to +21
def generate_token(length=32):
chars = string.ascii_letters + string.digits
return "".join(random.choice(chars) for _ in range(length))


def generate_reset_code():
return str(random.randint(100000, 999999))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`random` module is not suitable for generating security-sensitive tokens


The generate_token and generate_reset_code functions use the random module, which produces predictable, pseudorandom numbers. This is not suitable for security-sensitive contexts like session tokens or password reset codes, as an attacker could potentially guess the generated values.

Replace the use of the random module with the secrets module, which is designed for generating cryptographically strong random numbers suitable for managing secrets.

Comment thread app/utils.py
Comment on lines +24 to +30
def run_command(cmd):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout


def run_background(cmd):
subprocess.Popen(cmd, shell=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`subprocess.run` and `subprocess.Popen` with `shell=True` enable command injection


The run_command and run_background functions execute commands using shell=True, which passes the command through the system's shell. If an attacker can control any part of the cmd string, they can inject shell metacharacters (e.g., ;, &&, |) to execute arbitrary commands, leading to remote code execution.

To fix this, pass command arguments as a list and set shell=False. The function signatures should be changed to accept a list of arguments (e.g., run_command(cmd_list)), and all callers must be updated to provide arguments as a list.

Comment thread app/utils.py
Comment on lines +54 to +60
def log_request(method, path, user_id=None):
_request_log.append({
"method": method,
"path": path,
"user_id": user_id,
"timestamp": datetime.now().isoformat(),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global `_request_log` list grows indefinitely, risking memory exhaustion


The log_request function appends entries to the global _request_log list without any size limit. In a long-running service, this list will grow indefinitely, consuming all available memory and eventually causing a denial of service.

To prevent unbounded memory growth, use a data structure with a fixed maximum size, such as collections.deque(maxlen=N). This will store the N most recent log entries and automatically discard older ones.

Comment thread app/utils.py
Comment on lines +104 to +107
def ping_host(host):
"""Check if a host is reachable."""
output = run_command("ping -c 1 " + host)
return "1 packets received" in output or "1 received" in output

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-controlled input `host` is concatenated into a shell command


The host parameter is concatenated directly into the command string that is executed by run_command. Since run_command uses shell=True, an attacker can provide a malicious string for host (e.g., 8.8.8.8; rm -rf /) to execute arbitrary commands on the server.

To fix this, the command and its arguments should be passed as a list to run_command, and run_command should be modified to use shell=False. This prevents the shell from interpreting the input as commands.

Comment thread app/utils.py
Comment on lines +114 to +119
def build_url(base, path, params={}):
url = base.rstrip("/") + "/" + path.lstrip("/")
if params:
query = "&".join("%s=%s" % (k, v) for k, v in params.items())
url += "?" + query
return url

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query parameters are not URL-encoded, leading to malformed URLs


The build_url function constructs a query string by joining keys and values without URL-encoding them. If parameter values contain special characters like &, =, or ?, the resulting URL will be malformed, which can break functionality or lead to security issues.

Use urllib.parse.urlencode to safely construct the query string. This function correctly handles special characters, ensuring the generated URL is well-formed and secure.

Comment thread app/api.py
elif fmt == "csv":
lines = []
for row in results:
lines.append(",".join(str(v) for v in row.values()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual CSV creation without sanitization allows for CSV formula injection


The CSV export functionality joins data values with commas without any sanitization. If a data value contains a malicious formula (e.g., starting with =), spreadsheet applications like Excel or Google Sheets may execute it when the exported CSV is opened. This can lead to data exfiltration or phishing attacks.

Sanitize all data written to the CSV to prevent formula injection. At a minimum, prefix any value starting with =, +, -, or @ with a single quote (') to ensure it is treated as text by spreadsheet software. Using a standard CSV library like csv is also recommended for proper quoting.

Comment thread app/database.py
Comment on lines +31 to +35
conn = self._connect()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s" % user_id)
row = cursor.fetchone()
conn.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Database connection is not closed if an exception occurs


The database connection conn is not closed within a finally block or a with statement. If any operation on the cursor (e.g., cursor.execute()) raises an exception, the conn.close() line will be skipped, causing a resource leak.

To ensure connections are always closed, use the connection object as a context manager. For example: with self._connect() as conn: .... This guarantees that the connection is closed automatically, even if errors occur.

Comment thread app/file_processor.py
Comment on lines +56 to +57
for child in root:
data[child.tag] = child.text

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`process_xml` overwrites values for XML elements with the same tag


The code iterates through XML children and assigns child.text to data[child.tag]. If multiple children have the same tag (e.g., multiple <item> elements), the value for that key in the dictionary will be overwritten on each iteration, and only the text from the last element will be kept.

To preserve all data, check if the tag already exists as a key. If it does, convert the value to a list (if it isn't one already) and append the new value. This ensures that all values for a given tag are collected.

Suggested change
for child in root:
data[child.tag] = child.text
for child in root:
if child.tag not in data:
data[child.tag] = child.text
else:
if not isinstance(data[child.tag], list):
data[child.tag] = [data[child.tag]]
data[child.tag].append(child.text)

Autofix™ verified this patch. However, please review before accepting. AI can make mistakes.

Comment thread app/file_processor.py
Comment on lines +104 to +108
return {
"name": os.path.basename(filepath),
"size": os.path.getsize(filepath),
"extension": filepath.rsplit(".", 1)[-1],
"exists": os.path.exists(filepath),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`get_file_info` calls `os.path.getsize` without ensuring the file exists


The function returns os.path.exists(filepath) as one of the values, but it also calls os.path.getsize(filepath) unconditionally. If the file does not exist, os.path.getsize will raise a FileNotFoundError, causing a crash.

Check if the file exists first. If it does, get its size. If not, return a default value like 0 or None for the size to prevent the unhandled exception.

Comment thread app/database.py
Comment on lines +124 to +127
for record in records:
cols = ", ".join(record.keys())
vals = ", ".join("'%s'" % v for v in record.values())
cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % (table, cols, vals))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records are inserted one by one in a loop


The bulk_insert method iterates through a list of records and executes one INSERT statement for each record. This approach is very inefficient for large datasets, as it incurs significant overhead from repeated database round-trips.

For efficient bulk data insertion, use the cursor.executemany() method, which sends all the data to the database in a single operation.

Comment thread app/file_processor.py
Comment on lines +64 to +67
header = f.readline().strip().split(",")
for line in f:
values = line.strip().split(",")
rows.append(dict(zip(header, values)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual CSV parsing with `split(",")` is not robust


The code parses CSV files by splitting lines on commas. This approach is not robust and will fail for standard CSV features like quoted fields that contain commas, escaped quotes, or newlines, leading to data corruption and parsing errors.

To ensure correct parsing, use the standard library csv module, specifically csv.reader or csv.DictReader, which are designed to handle the complexities of the CSV format.

Comment thread app/models.py
Comment on lines +64 to +68
def __init__(self, name, description, price, tags=[]):
self.name = name
self.description = description
self.price = price
self.tags = tags

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`Item.__init__` uses a mutable default argument `[]` for `tags`


The tags parameter in Item.__init__ uses a mutable list as its default value. This means all Item instances created without explicitly providing tags will share the same list. Modifications to one instance's tags will unexpectedly affect all others.

Use None as the default and initialize a new list inside the method if tags is None to ensure each instance has its own list.

Comment thread app/utils.py


def hash_string(s):
return hashlib.md5(s.encode()).hexdigest()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`hashlib.md5` is cryptographically broken and should not be used


The hash_string function uses the MD5 hashing algorithm, which is cryptographically broken and vulnerable to collision attacks. Using MD5 for security-related purposes like data integrity checks or password hashing is highly insecure and can allow an attacker to forge data or crack hashes.

Replace hashlib.md5 with a modern, secure hashing algorithm such as hashlib.sha256. For password hashing, use a dedicated library like passlib which implements algorithms like Argon2 or scrypt.

Comment thread app/database.py
for record in records:
cols = ", ".join(record.keys())
vals = ", ".join("'%s'" % v for v in record.values())
cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % (table, cols, vals))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible SQL injection vector through string-based query construction.


Constructing SQL query using user provided data is insecure. It makes application vulnerable to [SQL injection](SQL injection) attacks.

Comment thread app/database.py
cursor = conn.cursor()
stats = {}
for table in ["users", "items", "config", "sessions"]:
cursor.execute("SELECT COUNT(*) FROM %s" % table)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting a regular string which could be a f-string


f-strings are the fastest way to format strings as compared to the following methods: * using format specifiers %

Comment thread app/database.py
cursor = conn.cursor()
stats = {}
for table in ["users", "items", "config", "sessions"]:
cursor.execute("SELECT COUNT(*) FROM %s" % table)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible SQL injection vector through string-based query construction.


Constructing SQL query using user provided data is insecure. It makes application vulnerable to [SQL injection](SQL injection) attacks.

Comment thread app/api.py


if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible binding to all interfaces.


Binding to all network interfaces can potentially open up a service to traffic on unintended interfaces, that may not be properly documented or secured. This can be prevented by changing the code so it explicitly only allows access from localhost.

Comment thread app/api.py
REST API handlers for the web service.
"""

import json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import json


An object has been imported but is not used anywhere in the file.
It should either be used or the import should be removed.

Comment thread app/utils.py


def clear_cache():
global _cache

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the global statement


It is recommended not to use global statement unless it is really necessary. Global variables are dangerous because they can be simultaneously accessed from multiple sections of a program. This frequently results in bugs. This also make code difficult to read, because they force you to search through multiple functions or even modules just to understand all the different locations where the global variable is used and modified. Read more about why it should be avoided here.

Comment thread app/utils.py
return a / b


def merge_dicts(base, override, defaults={}):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dangerous default value {} as argument


Do not use a mutable like list or dictionary as a default value to an argument. Python’s default arguments are evaluated once when the function is defined. Using a mutable default argument and mutating it will mutate that object for all future calls to the function as well.

Comment thread app/utils.py

def retry(fn, max_attempts=3, delay=1):
last_error = None
for attempt in range(max_attempts):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable 'attempt'


An unused variable takes up space in the code, and can lead to confusion, and it should be removed. If this variable is necessary, name the variable _ to indicate that it will be unused, or start the name with unused or _unused.

Comment thread app/utils.py
return os.environ.get(key, default)


def build_url(base, path, params={}):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dangerous default value {} as argument


Do not use a mutable like list or dictionary as a default value to an argument. Python’s default arguments are evaluated once when the function is defined. Using a mutable default argument and mutating it will mutate that object for all future calls to the function as well.

Comment thread app/utils.py
def build_url(base, path, params={}):
url = base.rstrip("/") + "/" + path.lstrip("/")
if params:
query = "&".join("%s=%s" % (k, v) for k, v in params.items())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting a regular string which could be a f-string


f-strings are the fastest way to format strings as compared to the following methods: * using format specifiers %

@vishnu-deepsource

Copy link
Copy Markdown

@deepsourcebot Review this?

@vishnu-deepsource

Copy link
Copy Markdown

@deepsourcebot Can ya review this please?

@vishnu-deepsource

Copy link
Copy Markdown

@deepsourcebot review this

@vishnu-deepsource

Copy link
Copy Markdown

@deepsourcebot, Brother review this

Comment thread app/api.py
Comment on lines +55 to +63
@app.route("/api/users", methods=["POST"])
def create_user():
try:
data = request.get_json()
username = data["username"]
password = data["password"]
email = data["email"]
role = data.get("role", "user")
result = db.create_user(username, password, email, role)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`db.create_user` is exposed without access control


The user-creation endpoint has no token validation, so anonymous callers can register arbitrary accounts and potentially assign elevated roles. This enables privilege escalation and account sprawl.
Add Authorization token checks with validate_session before db.create_user, and enforce server-side role restrictions

Comment thread app/api.py
Comment on lines +69 to +76
@app.route("/api/export", methods=["POST"])
def export_data():
"""Export data in the requested format."""
try:
data = request.get_json()
fmt = data.get("format", "json")
query = data.get("query")
results = db.search(query, 0, 1000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`db.search` export endpoint lacks authentication controls


/api/export accepts arbitrary queries without validating caller identity. Attackers can scrape large result sets and exfiltrate internal data.
Require token validation before executing db.search, and return 401 for anonymous requests

Comment thread app/api.py
Comment on lines +125 to +129
user = validate_session(token)
if user:
data = request.get_json()
for key in data:
db.set_config(key, data[key])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`validate_session` alone permits arbitrary `db.set_config` updates


Configuration updates are guarded only by session validity, not by privilege level. Any low-privilege account can alter critical settings and change application behavior.
Add explicit role/permission checks after validate_session and allowlist mutable keys before calling db.set_config

Comment thread app/file_processor.py
def process_xml(filepath):
"""Parse an XML file and return its structure."""
parser = ET.XMLParser()
tree = ET.parse(filepath, parser=parser)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`ET.parse` on untrusted XML allows entity expansion DoS


Parsing attacker-supplied XML with standard parsers exposes denial-of-service vectors through malicious document structures. A single payload can exhaust worker resources and disrupt normal processing.

Use defusedxml.ElementTree.parse for untrusted XML and reject documents exceeding size or node-count limits

Comment thread app/models.py
Comment on lines +145 to +146
def reset(self):
self._settings = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`reset` assigns instance `_settings`, diverging shared config state


set() and get() operate on _settings, initially class-shared. reset() rebinds self._settings, potentially splitting state between instance and class storage and causing inconsistent reads.

Update reset() to clear shared storage consistently via type(self)._settings = {} or _settings.clear()

Comment thread app/auth.py
Comment on lines +25 to +26
"INSERT INTO users (username, password, email) VALUES ('%s', '%s', '%s')"
% (username, password, email)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String-based SQL query with user data enables injection


Using string formatting for composing an SQL query leads to a high risk of SQL injection by allowing attacker-controlled data to modify query logic and access or damage data. The vulnerable line is where username, password, and email are directly inserted into the query string.

Use parameterized queries with placeholders such as %s and pass user inputs as separate parameters to cursor.execute() to safely separate code from data and prevent injection attacks.

Comment thread app/auth.py
conn = get_db()
cursor = conn.cursor()
cursor.execute(
"UPDATE users SET password='%s' WHERE username='%s'" % (new_password, username)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String-based SQL query concatenation enables injection attacks


The code uses string formatting to inject new_password and username directly into the SQL update statement, enabling attackers to craft inputs that alter the query logic or execute arbitrary SQL.
Use parameterized queries with placeholders (e.g., %s) and pass parameters separately to safely execute the query and avoid SQL injection risks.

Comment thread app/auth.py
Comment on lines +129 to +132
query = "SELECT permission FROM acl WHERE user_id=%s AND resource='%s'" % (
user_id,
resource,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String-based SQL query with user data enables injection


The SQL query is built using string interpolation with user-controlled resource, which can be manipulated to alter the intended query logic. Attackers could exploit this to execute arbitrary SQL commands, potentially accessing or damaging the database.

Replace string interpolation with parameterized queries using database adapter placeholders to safely pass user inputs and prevent injection attacks.

Comment thread app/database.py
)
else:
cursor.execute(
"INSERT INTO config (key, value) VALUES ('%s', '%s')" % (key, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using `%` operator is slower than `f-string` formatting


The use of the % operator for string formatting in the SQL insert statement is less efficient and slower compared to f-string formatting. This can impact performance especially in code with many string operations.

Replace the % formatting with an f-string to improve runtime performance and code readability, for example: f"INSERT INTO config (key, value) VALUES ('{key}', '{value}')".

Comment thread app/database.py
for record in records:
cols = ", ".join(record.keys())
vals = ", ".join("'%s'" % v for v in record.values())
cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % (table, cols, vals))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String-based query construction enables SQL injection


The line constructs an SQL query string using % operator with variables table, cols, and vals which may include untrusted input. Attackers can insert malicious SQL to manipulate the database or access unauthorized data.

Use parameterized queries or prepared statements supported by the database connector to safely include variables and prevent injection threats.

Comment thread app/api.py
Comment on lines +125 to +129
user = validate_session(token)
if user:
data = request.get_json()
for key in data:
db.set_config(key, data[key])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`validate_session`-only guard permits unauthorized configuration changes


update_config checks only that a session exists, not caller privileges. Normal users can alter global configuration and potentially disable safeguards or redirect behavior.

Add an explicit role/permission check before the update loop and return 403 when privilege requirements are not met

Comment thread app/database.py
def get_config(self, key):
conn = self._connect()
cursor = conn.cursor()
cursor.execute("SELECT value FROM config WHERE key='%s'" % key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interpolated config key enables injected `SELECT` conditions


Using direct interpolation in lookup SQL makes configuration reads attacker-steerable. This can leak sensitive operational settings or bypass key isolation.

Replace with SELECT value FROM config WHERE key = ? and bind key as a parameter

Comment thread app/database.py
Comment on lines +96 to +103
existing = self.get_config(key)
if existing is not None:
cursor.execute(
"UPDATE config SET value='%s' WHERE key='%s'" % (value, key)
)
else:
cursor.execute(
"INSERT INTO config (key, value) VALUES ('%s', '%s')" % (key, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read-then-write config flow causes race-driven duplicate writes


The two-step existence check is not atomic under concurrent access. Simultaneous updates can fail unpredictably or persist stale values.

Use a single atomic UPSERT statement inside one transaction to eliminate check-then-act races

Comment thread app/file_processor.py
Comment on lines +51 to +52
parser = ET.XMLParser()
tree = ET.parse(filepath, parser=parser)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`xml.etree` parsing permits XML entity expansion DoS


process_xml uses the standard XML parser directly on uploaded files. Crafted XML payloads can trigger expensive entity processing and cause request-time denial of service.

Use defusedxml.ElementTree for untrusted XML and enforce input size limits before parsing

Comment thread app/models.py
self.created_at = datetime.now()

def to_dict(self):
return self.__dict__

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`__dict__` return leaks sensitive attributes to API responses


Returning raw __dict__ creates an unsafe default serialization path. If called for user-facing payloads, private fields are exposed without any allowlist or redaction.

Use an explicit field allowlist in to_dict, and omit sensitive properties by default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants