# WikiFleaks — Senior Security & Code-Quality Audit

**Addon:** `plugin.video.wikifleaks` v0.0.24 — Arabic multi-source Kodi video add-on (80+ sites)
**Lineage:** fork of [`Kodi-vStream/venom-xbmc-addons`](https://github.com/Kodi-vStream/venom-xbmc-addons) (source headers throughout)
**Scope:** `~89K` LOC of the addon's own code (104 site scrapers, ~204 hoster resolvers, `resources/lib`, root scripts) + `~146K` LOC of vendored third-party deps under `repo/`.
**Method:** analysis only, no code changed. 18 parallel investigation units; every critical/high finding put through an independent adversarial *refutation* pass (reachability + Kodi-attacker-input lenses). Findings below carry their verification verdict.

> ⚠️ **Bottom line:** This addon is **remotely exploitable for arbitrary code execution** through more than one independent path, and ships a hands-off **auto-update channel that trusts an unauthenticated remote catalog** controlling every scrape URL. Treat installed instances as at-risk. The §"What I'd fix this week" list is the priority order.

---

## ✅ Remediation applied in v0.0.25 (behavior-preserving)

The code-execution sinks were fixed **without removing any streaming functionality** (TLS-fallback, the Cloudflare/RapidAPI bypass cascade, and shared API keys were deliberately left intact so playback keeps working). Verified: legitimate navigation/dispatch still calls through, while injection payloads and code-bearing strings are blocked; deployed tree byte-compiles.

| Fix | Files | What changed |
|---|---|---|
| **C1 / H3 — exec→safe dispatch** | `default.py` | `exec("oGui."+f+"()")` / `cHome` / `oSearch` replaced with a validated `getattr` dispatch (`_safe_call`); `function`/`site` must match `^[A-Za-z_]\w*$`. Legitimate method names pass; `;`/`(`/`.`/space payloads are rejected. |
| **C3 + H2 — eval→ast.literal_eval** | `resources/sites/livetv.py:568`, `resources/lib/config.py:255`, `resources/lib/tmdb.py:584/678/681` | Remote/metadata `eval()` replaced with `ast.literal_eval` + graceful fallback. Literal data (char-code lists, dict reprs) still parses; `__import__(...)`/calls are rejected. |
| **H3 (loaders)** | `resources/lib/handler/pluginHandler.py`, `resources/lib/handler/hosterHandler.py`, `resources/lib/gui/hoster.py:670` | `exec("from … import …")` / `__import__` with interpolated names replaced by `importlib.import_module` + identifier validation. `cHosterGui.getHoster` is URL-reachable (`play()`→`sHosterIdentifier`); now guarded. |
| **C2 — catalog fail-safe** | `resources/lib/comaddon.py` (`setUserProps`) | Remote `sites.json` is structurally validated before it replaces the active catalog; malformed/tampered payloads (or URL fields with newlines) are rejected and the **last-good catalog is kept**. The real 102-site catalog passes unchanged. Combined with the eval fix, the catalog→RCE chain is broken. |

**Deliberately *not* changed** (would break working playback or needs product decisions): the TLS auto-downgrade `BUG_SSL` (many stream hosts have broken certs), per-host `verify=False`, the RapidAPI bypass cascade, and the shared/embedded keys (H4/H6). These remain open items — see §"What I'd fix this week". Dormant code (`sucuri.py`, `stormwall.py`, `enregistrement.py`, `service.py` recording, `unjuice2.py`) was confirmed via git to be **inherited-dormant since the baseline import** (not a recent toggle) and left untouched.

*Applied locally and deployed to Kodi for testing (v0.0.25); not committed.*

---

## Severity summary

| Severity | Count (raw) | After verification |
|---|---|---|
| Critical | 7 | **4 confirmed** · 2 downgraded→High · 1 refuted (dead code) |
| High | 35 | ~28 confirmed/adjusted-real · ~7 refuted/defanged |
| Medium | 39 | quality + real-but-lower-impact |
| Low | 35 | hygiene / hardening |
| Info | 14 | architecture & feature ideas |

The raw machine counts (130 total: 7C/35H/39M/35L/14I) over-count because static pattern matches include dead code and defanged sinks. The verified picture is below; refuted items are kept in the **Appendix** for transparency (they are still worth deleting).

---

## Phase 0 — Project map

- **Type:** `xbmc.python.pluginsource` (video) with `library="default.py"`, plus `kodi.context.item` menu hooks and a `service.py` daemon (`addon.xml`). Targets **Kodi 19–21 (Omega)** / `xbmc.python 3.0.0` (Py3.11), with leftover Py2 shims.
- **Entry / trust boundary:** Kodi calls `default.py` with `sys.argv[2]` = the `plugin://` query string. `cInputParameterHandler` (`resources/lib/handler/inputParameterHandler.py:8-26`) splits it into a dict with **no sanitization** (only URL-unquoting). `default.py` reads `site` + `function` and dispatches. Plugin URLs are attacker-reachable via `.strm`/`.m3u` files, favourites, library/widget items, JSON-RPC `RunPlugin`/`Player.Open`, and any co-installed addon.
- **Network I/O:** centralized in `resources/lib/handler/requestHandler.py` (`cRequestHandler`) — 20s default timeout, optional `requests_cache` layer, an SSL-fallback that **silently disables TLS verification**, and a Cloudflare/RapidAPI "bypass" cascade. Many scrapers also call `requests`/`urllib` directly, bypassing it.
- **Settings input:** `resources/settings.xml` (credentials, tokens, API keys; several shipped as live defaults).
- **Filesystem / SQLite:** `resources/lib/db.py` (watched/resume/history), `tmdb.py`/`omdb.py` (metadata caches), `library.py`/`download.py` (writes `.strm`/`.nfo`/media), `requests_cache.db`.
- **Local servers:** `mCaptcha/mserver.py` (LAN-exposed), `proxy/ProxyHTTPRequestHandler.py` + `drm_proxy.py` (localhost).
- **Auto-update:** `resources/lib/update.py` pulls a remote `sites.json` hourly and applies it (`comaddon.py` `siteManager.setUserProps`).

---

## Phase 1 — Security findings

### 🔴 CRITICAL

#### C1 — Remote code execution via `exec()` on the attacker-controlled `function` parameter
**`default.py:213-218` (`cGui`) and `default.py:273-278` (`cHome`)** · category: rce · **verdict: CONFIRMED (reachability + kodi-input), error-free RCE proven**

```python
def isGui(sSiteName, sFunction):
    if sSiteName == 'cGui':
        oGui = cGui()
        exec("oGui." + sFunction + "()")     # sFunction is the raw URL 'function' param
```

`sFunction = oInputParameterHandler.getValue('function')` (`default.py:43`) is the unsanitized `function` query parameter, URL-decoded (`inputParameterHandler.py:19-26`). It is string-interpolated into `exec()`, so the attacker is **not** limited to no-arg methods. The query parser splits on `&`/`=` *before* URL-decoding, so an encoded payload survives intact. Verified working PoC:

```
plugin://plugin.video.wikifleaks/?site=cGui&function=updateDirectory()%3B__import__(%22os%22).system(%22id%22)%23
```
→ `exec('oGui.updateDirectory();__import__("os").system("id")#()')` — the trailing `()` is commented out by `#`; the command runs and the function returns with **no error**. Same primitive at `cHome` (line 276).

**Why exploitable in Kodi:** any `.strm`/favourite/widget/library item, a JSON-RPC `RunPlugin(...)` call, or a co-installed addon that opens such a URL triggers it — no user trust beyond opening a media item. This is the canonical vStream exec-dispatch RCE.

**Remediation:** never `exec` a name from input. Allow-list dispatch: `if sFunction in ALLOWED_METHODS and re.fullmatch(r'[A-Za-z_]\w*', sFunction): getattr(oGui, sFunction)()`. Eliminate `exec` from `default.py` entirely (also `:293`, a constant `exec("oSearch.searchGlobal()")`).

---

#### C2 — Fetch-and-trust auto-update of the remote site catalog (the supply-chain master key)
**`resources/lib/update.py:28-33` + `resources/lib/comaddon.py:618-620` + `requestHandler.py:235-237`** · category: supply-chain · **verdict: CONFIRMED (reachability + kodi-input)**

```python
sUrl = 'https://raw.githubusercontent.com/WikiFleaks/Dist/master/.../sites.json'
properties = cRequestHandler(sUrl).request(jsonDecode=True)
if properties == "": return
siteManager().setUserProps(properties)          # setUserProps: self.data = props; self.save()  — no validation
```

`service.py:22` calls `cUpdate().getUpdateSetting()` at daemon startup; `auto_update_sites` defaults `"true"` (`settings.xml:63`) and `update.py:14-16` forces an old timestamp so the hourly fetch fires on first run. The decoded JSON is applied **verbatim** — no schema, no URL allow-list, no signature — and `setUserProps` overwrites the *entire* catalog. Every scraper then reads its base URL from this data (`URL_MAIN = siteManager().getUrlMain(...)`) and feeds the scraped HTML into the `eval()` sinks (C3, H-eval). The fetch rides `cRequestHandler`, which **auto-downgrades TLS** (C5/H1 below), so a network MITM — or a compromise of the `WikiFleaks/Dist` GitHub repo — can rewrite every site's `url`, redirect all scraping to attacker servers, and reach code-execution sinks.

**Remediation:** treat the catalog as untrusted. Ship & verify a detached **Ed25519 signature** (public key baked into the addon) before applying; validate against a strict JSON schema; enforce `https` + a known-domain allow-list on every `url`/`url2`; fail closed (keep the last-good catalog) on any error. Pin the fetch to `verify=True` with no downgrade.

---

#### C3 — RCE via `eval()` of unpacked remote JavaScript (`sawlive` livetv path)
**`resources/sites/livetv.py:564-572`** · category: eval · **verdict: CONFIRMED (both lenses)**

```python
strs = cPacker().unpack(str2)                       # unpack remote packed JS
aResult1 = re.findall('var .+?=([^;]+);', strs)
if aResult1:
    jameiei = eval(aResult1[0])                     # eval() of attacker-controlled RHS
```

`showHosters()` (a registered menu action) follows a fixed chain to **`http://www.sawlive.tv/...`** (hardcoded plaintext — `livetv.py:553`), unpacks the `eval(function(p,a,c,k,e,d))` block, then `eval()`s the RHS of the first `var x=…;`. The author expects a list of ints (iterated as `chr(c)`) but `eval` imposes no such limit: any operator of, or **MITM on** (cleartext upstream), the stream host can serve `__import__('os').system(...)` and execute arbitrary Python in the Kodi process.

**Remediation:** `ast.literal_eval(aResult1[0])` inside `try/except`; reject anything that is not a list of ints before `chr()`.

---

#### C4 — RCE via Node.js execution of scraped obfuscated JS with an escapable "sandbox"
**`resources/sites/faselhd.py:638-681`** · category: rce · **verdict: CONFIRMED — escape reproduced empirically**

```python
"var _ps=process;"            # real process saved BEFORE the sandbox is nulled
"var process=undefined;"
"try{" + js_code + "}catch(e){}"   # scraped JS concatenated raw, same scope as _ps
...
subprocess.run([node_bin, '-e', node_wrapper], capture_output=True, text=True, timeout=15)
```

`_resolvePlayerQualities()` fetches the remote player page, regex-extracts the `<script>` body, and passes it **unsanitized** into a Node wrapper run with `node -e`. The wrapper sets `require`/`module`/`process` to `undefined` *after* saving the genuine `process` into `_ps` in the same scope — so `_ps.mainModule.require('child_process').execSync(...)` escapes. The verifier built the exact wrapper and confirmed it writes a file / runs commands (rc=0). **Precondition:** Node.js installed on the host (common on dev/server installs; some Android/AppleTV builds lack it).

**Remediation:** do not execute scraped JS. Use a pure-Python static unpacker (the addon already has `decode_page()` for the `adilbo` variant). If JS execution is unavoidable, use a genuinely isolated VM (separate process, frozen context, no network, dropped privileges) — never share the real `process` object.

> **Severity note:** the workflow rated C1/C3/C4 as *High* by its own rubric. I elevate them to **Critical** because the input is fully attacker-controlled with no realistic precondition (C1/C3) and the chain is end-to-end proven. C2 is the highest-leverage issue because it is the single remote control point over every other scrape.

**Downgraded from raw-critical (still serious):** C5 (TLS auto-downgrade → High H1) and the RapidAPI exfiltration (→ High H6). **Refuted raw-criticals** (`sucuri.py` exec, `unjuice2.py` eval, `enregistrement.py` command-injection) are dead/unreachable — see Appendix.

---

### 🟠 HIGH (selected; ~28 confirmed)

#### H1 — Automatic TLS-verification downgrade on certificate failure *(systemic, in the shared handler)*
**`requestHandler.py:190-191, 235-237, 253-256, 215`** · category: tls · **verdict: CONFIRMED**
```python
except ConnectionError as e:
    if 'CERTIFICATE_VERIFY_FAILED' in str(e) and self.BUG_SSL == False:
        self.BUG_SSL = True
        return self.__callRequest(jsonDecode)   # retry with verify=False
```
Default is `verify=True`, but **any** cert error makes the handler silently retry with verification off. A passive/active MITM presents a bad cert once and defeats TLS for that request — across the addon's entire HTTP layer, including the C2 catalog fetch and metadata feeding `eval`. **Fix:** remove the downgrade; surface a hard error. Never retry insecurely.

#### H2 — `eval()` of remote metadata (defense-in-depth + confirmed reachable subset)
**`resources/lib/config.py:255` (`eval(meta['guest_stars'])`), `resources/lib/tmdb.py:583-584` (`eval(listeGenre)`)** · category: eval · **verdict: CONFIRMED** (note: `tmdb.py:678/681` crew eval **refuted** — data is pre-parsed JSON). Combined with H1/C2, attacker-influenced metadata reaches `eval`. **Fix:** `ast.literal_eval` / `json.loads`.

#### H3 — Attacker-controlled `function` → `getattr` on site modules calls any module-level callable
**`default.py:161-164`** · category: rce · **verdict: CONFIRMED**
```python
plugins = __import__('resources.sites.%s' % sSiteName, fromlist=[sSiteName])
function = getattr(plugins, sFunction)   # sFunction unvalidated
function()
```
Lets an attacker invoke any top-level function in any `resources/sites/*` module by name. The `exec`-based variants in `pluginHandler.py:43-52` and `hosterHandler.py:21-24` (Low) are the same anti-pattern over on-disk filenames. **Fix:** allow-list + `importlib`.

#### H4 — URL + **all session headers (cookies/tokens)** exfiltrated to third-party RapidAPI "bypass" services *(also SSRF)*
**`requestHandler.py:272-440`** · category: ssrf/data-leak · **verdict: CONFIRMED (downgraded from Critical)**
On **any** 403/503 (trivially attacker-induced), the handler ships `self.__sUrl` **and the entire `requests` session header set** (Referer/Cookie/auth that callers add) to `scrapeninja`/`scrappey`/`cloudflare-bypass7` on RapidAPI, which also fetch the URL server-side. No opt-in gates this path. **Fix:** strip to a `User-Agent`-only allow-list; require explicit informed opt-in; never proxy cookie-bearing requests off-device.

#### H5 — LAN-exposed, unauthenticated captcha server that writes attacker data into addon settings
**`resources/lib/mCaptcha/mserver.py:21-22, 30-40, 70-84, 117-147`** · category: local-server · **verdict: CONFIRMED (mechanics) / severity adjusted**
`get_ip()` deliberately resolves the **LAN-routable IP** (connects to `8.8.8.8`, not `127.0.0.1`); the server binds `ThreadingTCPServer((LAN_IP, 9001))` and the UI tells the user to browse it from another device. `do_GET` has **no auth / Origin / Host check**: any path containing `response` accepts a `token` query param, base64/JSON-decodes it, and writes it straight into persistent addon settings (Cloudflare clearance cookie / mcaptcha token). Anyone on the LAN — or a web page via DNS-rebinding/CSRF — can poison the addon's stored session. **Fix:** bind `127.0.0.1`, require an unguessable per-session path/token, validate `Origin`/`Host`, shut the server down promptly.

#### H6 — Hardcoded / shipped secrets *(multiple, all confirmed)*
- **`resources/lib/trakt.py:25-26`** — Trakt OAuth `API_KEY` + `API_SECRET` in source.
- **`resources/settings.xml:85`** — a **live shared RapidAPI key** (`a8526713f4msh...`) as the default for every install.
- **`resources/lib/trakt.py:1386`** + **`settings.xml:30-31`** — TMDb / OMDb API keys.
- **`resources/sites/showb.py:357,390,460,491`** — **real febbox account JWTs** (decoded: valid `uid`/`token`, exp 2025-2026) shipped to every install.
- **`settings.xml:130-188`** — hoster usernames/passwords/tokens stored as plaintext `type="text"`.

A client-distributed OAuth secret is inherently non-secret: impact is **quota theft, key abuse, and forced revocation** breaking all users, plus account compromise for the embedded febbox tokens. **Fix:** rotate everything that is live; remove embedded credentials; proxy Trakt/RapidAPI through a server you control or require user-supplied keys; mark secret settings as masked/hidden.

#### H7 — Systemic `verify=False` in hosters & sites
Confirmed-reachable examples: **`hosters/mycima.py:26,40,58`** (also propagates `verifypeer=false` to the player), **`hosters/vidup.py:33-34`** (raw `SSLContext`, no hostname check), **`hosters/thevideo_me.py:42-43`** (`PROTOCOL_TLSv1`), **`hosters/vidyard.py:35,53`**, **`sites/tma.py:360-380,582`**, and a systemic `verifypeer=false` across many `hosters/*` (`abstream.py:43` representative). **Fix:** default to verification on; only relax per-host with a logged, user-visible exception.

#### H8 — Cleartext / credential-leaking auth flows
- **`resources/lib/recaptcha_v2.py:155,189`** — captcha challenge fetched and token submitted over **`http://`** *with* `verify=False`.
- **`resources/sites/iptv.py:28-31,253,280,349`** — IPTV auth + token exchange over plaintext HTTP; **`:303-318`** sends account creds + site key to a third-party RapidAPI captcha solver; **`:241-243`** persists the password back into settings.
- **`resources/sites/linkbox.py:393-402`** — username & password sent as **plaintext URL query params**.
- **`resources/lib/handler/premiumHandler.py:89-92`** — hoster login posts credentials over plaintext HTTP.

#### H9 — `requests-cache 0.5.2` SQLite backend deserializes cache entries via `pickle.loads` (local RCE)
**`repo/script.module.requests-cache/lib/.../dbdict.py:161-169`** · category: deserialization · **verdict: CONFIRMED**
The addon's `CachedSession` (`requestHandler.py:168`) uses the on-disk `requests_cache.db`. Every cache read goes through `pickle.loads`. Any process/addon able to write that file gets code execution on the next cached read. **Fix:** upgrade to `requests-cache 1.2.x` with the JSON/cattrs serializer (removes the pickle surface) and migrate the `CachedSession` call to the 1.x API.

#### H10 — Other confirmed/adjusted highs
- **`.strm` library write** — `resources/lib/library.py:57-100`: filename/`.strm` content built from remote `sFileName`/`sMediaUrl` (reachable via "Add to library" context menu), partial sanitizers; the `.strm` holds a `plugin://` URL that **re-enters** the C1 dispatch (content-injection → dispatch). Sanitize to a filename allow-list; validate the embedded URL.
- **Stalker / set_stalker** — `resources/lib/set_stalker.py:51-67`: 16 hardcoded third-party IPTV portals over plaintext HTTP with bundled MAC credentials.

---

### 🟡 MEDIUM (39 — highlights)
- **`requestHandler.py:292`** — 503/403 bypass uses `urllib.request.urlopen` with **no timeout** → can hang the thread (also `runscript.py:46/64/92`).
- **`requestHandler.py:170-172, 509-538`** — global `socket.getaddrinfo` monkeypatch with hardcoded public DNS; **not thread-safe** under the `ThreadingHTTPServer` proxy (`player-service`).
- **`resources/lib/proxy/ProxyHTTPRequestHandler.py:14-32`** — localhost HLS proxy is an unauthenticated open relay (arbitrary `u=` URL + headers), and forcibly rewrites `https→http` (TLS downgrade).
- **`resources/lib/cloudflare.py:150-156, 219-220`** — logs full cookies/post-data/headers to the Kodi log.
- **`resources/lib/player.py:129-219`** — resolved playback URL passed to `setResolvedUrl`/`Play` with no scheme/content validation.
- **`resources/lib/upnext.py:182-187`** — re-dispatches arbitrary site module + function via `__import__`/`getattr` on resolver-supplied names.
- **`resources/sites/sflix.py:563-572`** / **`hosters/chillx.py:38-42`** — stream payload/keys exfiltrated to a third-party Apps Script; AES key fetched at runtime from raw GitHub.
- **`resources/hosters/linkbox.py` / `gofile.py` / `uptostream.py`** — account tokens placed in URL query strings / forwarded to the player.
- **`resources/hosters/torrent.py:49,56-61`** — remote content written to a fixed addon path.
- Plus: bare-`except` swallowing (see Phase 3), `tmdb.py` string-formatted SQL on `title` (Low, defanged), OMDb key in plaintext URL + logs.

### 🔵 LOW (35 — hygiene/hardening)
`exec` with interpolated names in the plugin/hoster loaders; `eval`/`int(eval())` in latent code paths; secrets/auth outcomes logged via `VSlog`; cleartext `http://` scattered through `livetv.py`, `rutube.py`, `mailru.py`; DRM key with auth token written to a world-readable file (`sites/awaan.py:187-191`); hardcoded CSRF token (`sites/tma.py:449`); blocking `xbmc.sleep` loops; duplicated `cmf2`/`cmf3`/`unjuice`/`recaptcha` modules; 166 `print()` statements; Py2 cruft. Full list in the workflow output.

---

## Phase 2 — Dependency & supply-chain

Vendored versions (grounded in files read): **cloudscraper 1.2.71**, **dnspython 2.8.0** *(ahead of any real release as of knowledge cutoff — verify provenance / possible mis-tagged build)*, **requests-cache 0.5.2** ⚠️, **requests-toolbelt 1.0.0**, **pycryptodome 3.23.0**, **resolveurl 5.1.195**, **kodi-six 0.1.3.1**, **pyqrcode 1.2.1**. `requests` is *not* vendored but `requests-cache`'s addon.xml pins **`requests 2.22.0+matrix.1`** (CVE-2023-32681 proxy-auth leak, CVE-2024-35195 cert-verify).

| Issue | Where | Action |
|---|---|---|
| **`requests-cache 0.5.2` pickle cache → local RCE** (H9) | `dbdict.py:161-169` | Upgrade → **1.2.1**, JSON/cattrs serializer; migrate API |
| Old `requests 2.22.0` (known CVEs) | `requests-cache/addon.xml:8` | Pin **`requests 2.32.3`**, `urllib3 2.2.x` |
| **tar-slip** in build tooling | `repo/update_deps.py:316` (the *tarfile* `extractall`; the ZIP sinks `266/348/385` are **not** vulnerable — CPython sanitizes `../`) | Use a sanitized extractor; verify wheel/sdist hashes vs PyPI digests |
| pycryptodome ships **macOS-only `.so`** binaries | `repo/script.module.pycryptodome/lib/Crypto/.../*.abi3.so` | Re-vendor manylinux/Windows/Android wheels — current binaries won't load on Kodi targets |
| cloudscraper bundles `js2py`/`nodejs` interpreters | `cloudscraper/interpreters/*` | Pin `interpreter='native'`; never select `js2py` (sandbox-escape CVEs) |
| Dead/broken `cloudscraper2` import | `requestHandler.py:308` | Remove (swallowed by bare `except`) |
| `deploy.py` `subprocess(shell=True)` | `deploy.py:32,45,100` | Low (local dev) — use arg lists |

---

## Phase 3 — Code quality & Kodi-specific

**Quality (systemic):**
- **278 bare `except:`** + 158 broad `except Exception` (77 silent `except: pass`) swallow `KeyboardInterrupt`/`SystemExit` and hide real failures. Worst: `tmdb.py`, `jsparser.py`, `runscript.py`, `download.py`, `config.py`, `cmf3.py`, `trakt.py`.
- **Ad-hoc network calls bypass the central handler** — only 2 of ~47 direct `requests` calls pass a timeout; the 503/403 `urlopen` has none. → hangs.
- **Heavy duplication** — `cmf2.py`≈`cmf3.py` (~146 differing lines), `unjuice`/`unjuice2`, `recaptcha`/`recaptcha_v2` + an 8,749-line vendored `librecaptcha/` tree. The JS-deobfuscators (`jsparser`, `tinyjsparser`, `jjdecode`, `aadecode`, `packer`, …) total ~6,143 lines in-tree.
- **Py2/3 cruft** — 93 `isMatrix/PY2` gates; `isMatrix()` does a fragile lexical compare `version[0:2] >= '19'`; lingering `import six` / `urllib2` aliases.

**Kodi-specific:**
- **`gui/gui.py:397-407`** — deprecated `ListItem.setInfo` (use `InfoTagVideo` setters on Kodi 19+/21); version-gated by lexical compare.
- **`gui/hoster.py:740-749`** — failure paths skip `setResolvedUrl(handle, False, ...)` → spinner hangs on resolve failure.
- **`player.py:129,190,198`** — ListItem path/headers passed as bytes; **`gui/gui.py:333-339,635-643`** — threads mutate a shared ListItem (race).
- **i18n / Arabic RTL** — many **hardcoded Arabic strings** in `gui.py` and `requestHandler.py:316-482` bypass `strings.po`; `settings.xml` carries raw Arabic labels on a deprecated schema; **no RTL/bidi handling** for mixed Arabic+Latin labels; `InfoTag` raises on non-numeric scraped values (`gui.py:417-427`).

---

## Phase 4 — Architecture & feature ideas

The runtime is the classic vStream "stringly-typed RPC": a hand-maintained `isFav/isWatched/…` predicate ladder + `__import__`+`getattr(sFunction)` + magic module globals (`SITE_NAME`/`SITE_IDENTIFIER`), with `exec`/`eval` used as control flow *and* as a JSON parser. There are **no tests** in the addon's own code, and import-time side effects (`default.py` runs `main()` at import; `requestHandler.py:21-23` reads settings at class-definition time) make modules un-importable under pytest without a Kodi shim.

Recommended structural moves (all consolidation, not greenfield):
1. **Replace exec/eval dispatch** with a `{name: class}` registry + `importlib.import_module`, validating `function` against an explicit set. Kills C1/H3 and the loaders.
2. **Define a `BaseProvider` abstraction** (`identifier, name, capabilities, search(), list(category,page), resolve(item)`); each site subclasses it; `siteManager` (the natural seam, `comaddon.py:495-666`) registers them.
3. **Metadata/artwork service** over the existing TMDb/OMDb clients (`resources/lib/metadata.py`) that uses `requests_cache` and **does not `eval()` its cache**.
4. **Unify watched-state** — reconcile the local `cDb` watched table with the existing Trakt client for cross-device sync.
5. **Consolidate search** — `default.py:298-313` is a dead duplicate of `cSearch._pluginSearch`.
6. **Licensed-source catalog** — add a `license`/`official` flag in `sites.json` + a setting that filters `getAvailablePlugins` to legal sources (same framework, legitimate distribution).
7. **Arabic RTL polish** — bidi isolates around mixed labels, right-aligned Arabic-locale skin, move all strings to `strings.po`.
8. **Add a `tests/` package** with a `kodistubs`/xbmc shim; start with pure-logic units (`inputParameterHandler` parsing, `util` quoting, `siteManager`).

---

## ✅ What I'd fix this week (priority order)

1. **Kill the `exec`/`getattr` dispatch RCE (C1, H3).** Allow-list method/site names; remove `exec` from `default.py`, `pluginHandler.py`, `hosterHandler.py`. *(Highest reachability, lowest effort.)*
2. **Remove the TLS auto-downgrade (H1)** in `requestHandler.py` (the `BUG_SSL` retry) and the per-host `verify=False`/`disableSSL` calls (H7). Fail closed.
3. **Lock down the auto-update channel (C2):** sign `sites.json` (Ed25519, key in-addon), validate schema + https-only URL allow-list, fail closed; pin the fetch to `verify=True`.
4. **Replace every `eval()` of remote/scraped data (C3, H2)** with `ast.literal_eval`/`json.loads`; remove the `node -e` JS execution (C4) in `faselhd.py`.
5. **Rotate & remove shipped secrets (H6):** Trakt client_id/secret, the default RapidAPI key, the febbox JWTs, TMDb/OMDb keys; require user-supplied keys or a proxy.
6. **Bind `mserver.py` to `127.0.0.1` + add a per-session token (H5);** stop forwarding cookies/auth headers to RapidAPI bypass services (H4).
7. **Upgrade `requests-cache` → 1.2.x (drop pickle) and `requests` → 2.32.3 (H9 + CVEs).**
8. **Add request timeouts everywhere** (kill the no-timeout `urlopen`) and stop logging cookies/tokens (`cloudflare.py`).

*Then:* delete dead/unreachable risky code (Appendix), de-duplicate `cmf2/cmf3` etc., and begin the provider-abstraction refactor.

---

## Appendix — Refuted / defanged static findings (kept for transparency)

These matched dangerous patterns but the adversarial pass found them **not exploitable as-is**. Still worth deleting/hardening to prevent regressions.

| Finding | File | Why not exploitable |
|---|---|---|
| `exec(s)` Sucuri cookie decoder | `resources/lib/sucuri.py:66-87` | `SucurieBypass` class is **never imported/used** anywhere — dead code. |
| `htmlrequest()` `disableSSL` | `resources/lib/stormwall.py:120-131` | Same — handler is **unreachable** dead code. |
| Recording → `.py` command injection | `resources/lib/enregistrement.py:46-57` | `cEnregistremement` is **never instantiated**. |
| `subprocess.Popen('python ' + path)` | `service.py:49-57` | Unconditional `return` at `service.py:29` makes the recording block **dead code**. |
| `eval(code)` JuicyCodes | `resources/lib/unjuice2.py:21` | Capture regex `[^\)]+` **forbids `)`** → no function call possible in the eval'd expression. |
| SQLi via watched/history queries | `resources/lib/db.py` (many) | Titles pre-sanitized by `re.sub('[^a-z0-9]', ' ', …)` — quotes/`;` stripped before reaching `%`-format queries. Still: switch to parameterized queries. |
| `eval(str(meta['crew']))` | `resources/lib/tmdb.py:678,681` | `meta` is already `oResponse.json()`-parsed (a Python list/dict repr), not raw attacker text. Still: use `ast.literal_eval`. |
| ZIP `extractall` | `repo/update_deps.py:266,348,385` | CPython `ZipFile` strips `..`/abs paths. (The **tarfile** path `:316` *is* a real tar-slip.) |

---
*Audit performed read-only. File:line citations are against the working tree at the time of review. The full machine-readable finding set (130 items with verbatim evidence and verifier reasoning) is preserved in the workflow output.*
