Skip to content

Fix bugs, security issues and performance problems#404

Open
KAMI911 wants to merge 18 commits intolinuxmint:masterfrom
KAMI911:fix/bugs-and-security
Open

Fix bugs, security issues and performance problems#404
KAMI911 wants to merge 18 commits intolinuxmint:masterfrom
KAMI911:fix/bugs-and-security

Conversation

@KAMI911
Copy link

@KAMI911 KAMI911 commented Mar 14, 2026

Summary

This PR addresses bugs, security issues, and minor quality problems found across common.py, hypnotix.py, and xtream.py.

common.py

  • Shell injectionos.system("mkdir -p '%s'" % PROVIDERS_PATH) replaced with os.makedirs(exist_ok=True)
  • Logic bugdownloaded_bytes was incremented by the fixed 4 MB block constant instead of the actual number of bytes received, causing the size check to almost always fail and delete the freshly-downloaded playlist
  • Crash on malformed settings – provider info is now validated to have exactly 6 fields before unpacking, with a descriptive error instead of a bare ValueError
  • First-run crashload_favorites() raised FileNotFoundError before any favorites existed; save_favorites() assumed the parent directory was already present

hypnotix.py

  • Credential exposureplay_async() logged the full channel URL (which embeds Xtream username/password); the reload() exception handler logged the raw provider_info string (same credentials)
  • Bare except clauses – two except: pass blocks replaced with except Exception: pass to avoid swallowing SystemExit/KeyboardInterrupt
  • Process-wide os.chdir()update_ytdlp() called os.chdir() which changes the working directory for the entire process; replaced with absolute paths and os.chmod()
  • Search debounce timerGLib.timeout_add_seconds(0.1, ...) silently truncated 0.1 to 0 (integer required), firing immediately with no debounce; replaced with GLib.timeout_add(100, ...)
  • Crash when no provider selectedon_search() accessed self.active_provider.channels without a None guard
  • Pango markup injectionset_markup("<b>%s</b>" % provider.name) would raise a GTK critical error if the provider name contained &, <, or >; fixed with GLib.markup_escape_text()
  • File handle leak – GPL licence file opened without a context manager and read line-by-line in a loop; replaced with with/read()
  • type() == anti-pattern – two not type(params) == dict checks replaced with not isinstance(params, dict)

xtream.py

  • Assignment typoself.cache_path == "" (comparison) should be self.cache_path = ""; the fallback to ~/.xtream-cache/ never triggered
  • Always-true conditionelif "Live": is always True; fixed to elif stream_type == "Live":
  • Shared mutable class attributesgroups, channels, series, movies, auth_data, authorization, state, and catch_all_group were class-level mutable objects shared across all instances; moved to __init__
  • UnboundLocalError on series streamsnew_channel.group_id was accessed unconditionally after an if/else where only the non-series branch creates new_channel; guard moved inside the live/vod branches
  • Duplicate catch_all groupself.catch_all_group was appended at the start of each stream-type iteration and a new catch_all Group was appended at the end, producing up to six catch_all entries; removed the redundant prepend
  • Redundant regex dispatchre.match(compiled_regex, name) replaced with regex.match(name); removed dead if search_result is not None guard (always True)

KAMI911 added 18 commits March 14, 2026 12:39
os.system("mkdir -p '%s'" % PROVIDERS_PATH) is a shell injection risk:
if PROVIDERS_PATH ever contains shell metacharacters the shell would
execute arbitrary commands.  Replace with os.makedirs(exist_ok=True)
which is both safer and avoids spawning a shell process.
provider_info.split(":::") with no bounds check raises a confusing
ValueError/TypeError on malformed data and makes it impossible to
distinguish corrupt settings from other errors.  Count the fields
first and raise a descriptive ValueError so the caller (reload())
can log and skip the entry cleanly.
downloaded_bytes was incremented by the fixed block_bytes constant
(4 MB) on every iteration rather than by the actual number of bytes
received.  The final size check therefore almost always reported an
incorrect file size and deleted the freshly-downloaded playlist.
Fix by incrementing by len(data) for byte chunks and by
len(data.encode('utf-8')) for already-decoded string chunks.
Also remove the spurious per-block print statement.
load_favorites() raised FileNotFoundError on first run before any
favorites were ever saved.  Guard with an existence check and return
an empty list instead.
save_favorites() assumed the parent directory already existed, which
also fails on first run.  Add os.makedirs(exist_ok=True) before
opening the file for writing.
Two places logged sensitive information to stdout:
1. play_async() printed the full channel URL, which for Xtream
   providers contains the username and password embedded in the path
   (e.g. /live/user/pass/id.ts).
2. The reload() exception handler printed the raw provider_info
   string, which also contains the stored username and password.
Remove the URL from the channel log line and replace the provider_info
dump with a generic message.
Two bare 'except: pass' blocks swallow all exceptions including
SystemExit, KeyboardInterrupt, and GeneratorExit, which can prevent
the application from shutting down cleanly.  Replace with
'except Exception: pass' to only catch regular exceptions.
os.chdir() changes the working directory for the entire process, so
any code running concurrently or afterwards that relies on the CWD
would break.  Relative paths like './yt-dlp' also fail if the CWD
changes again before the next call.
Build an absolute path with os.path.join() and pass it directly to
subprocess.getoutput() as a list (no shell involved).  Replace the
shell 'chmod a+rx' invocation with os.chmod() for the same reason.
GLib.timeout_add_seconds() requires an integer number of seconds.
Passing 0.1 silently truncates to 0, which fires the callback
immediately with no debounce delay.
Replace with GLib.timeout_add(100, ...) which takes milliseconds,
providing the intended 100 ms debounce.
on_search() accessed self.active_provider.channels without checking
whether active_provider is None.  If the user typed in the search bar
before any provider was loaded or selected, this raised an
AttributeError.  Guard with an early return that re-enables the
search bar so the user is not left with a frozen input.
set_markup("<b>%s</b>" % provider.name) passes the raw provider name
into a Pango markup string.  A name containing '&', '<', or '>'
causes a Pango parse error and a GTK critical warning, breaking the
providers page layout.
Wrap the name with GLib.markup_escape_text() before interpolation.
The open_about() method opened the GPL file without a context manager,
read it line-by-line into a list, then concatenated all lines in a
loop.  If an exception occurred mid-loop the file handle would leak.

Replace with a 'with' block and a single h.read() call.
'not type(params) == dict' uses identity comparison on a type object.
It does not recognise dict subclasses and reads less clearly than the
idiomatic 'not isinstance(params, dict)'.  Fix both occurrences in
on_video_params() and on_audio_params().
'self.cache_path == ""' is a comparison that evaluates and discards
True/False; it does not reassign the attribute.  The intended
assignment 'self.cache_path = ""' was never executed, so an invalid
cache directory was never replaced with the default ~/.xtream-cache/.
'elif "Live":' evaluates the non-empty string literal as a boolean,
which is always True.  This means the else branch that prints an
"Unrecognized stream type" warning can never be reached, and any
stream type that is not "VOD" or "Series" silently becomes TV_GROUP
instead of being reported as an error.

Fix: 'elif stream_type == "Live":'
groups, channels, series, movies, auth_data, authorization, state,
and catch_all_group were declared as class-level attributes with
mutable default values (lists and dicts).  In Python these are shared
across all instances of the class, so a second XTream instance would
inherit and mutate the first instance's data.

Move all mutable attributes into __init__ so each instance gets its
own independent copy.  Also pass "Live" as the stream_type for
catch_all_group instead of the empty string that previously produced
an "Unrecognized stream type" warning on every construction.
After the if/else block that creates either new_series (series branch)
or new_channel (live/vod branch), the code unconditionally evaluated
new_channel.group_id.  When loading series streams new_channel was
never assigned, causing an UnboundLocalError crash on every series
stream entry.

Move the group_id == "9999" debug print inside the live and vod
branches where new_channel is guaranteed to exist.
load_iptv() appended self.catch_all_group (the class-level instance)
to self.groups at the start of each stream-type iteration, then
appended a second freshly-constructed catch_all Group at the end of
the same iteration.  With three stream types (Live, VOD, Series) this
produced up to six catch_all entries in self.groups.

The per-stream-type Group added at the end of the loop is sufficient;
remove the redundant prepend.
re.match(compiled_regex, string) re-enters the module to dispatch back
to the compiled pattern object.  Call regex.match(string) directly on
the compiled object to skip the redundant dispatch.

Also remove the dead 'if search_result is not None' guard: search_result
is always a list and therefore never None, so the condition was always
True and the inner block always executed.
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.

1 participant