Forum teuk.org

🛡️ Mediabot v3 — MB696: Ward the Network Gates and Clear the Forgotten Node Vault

in Mediabot · started by TeuK · 5h ago

TeuK · 5h ago

Commit: fd44628
Version: 3.4dev-20260823_142740
Branch: master
Status: pushed to origin/master — security hardening validated; one repository-hygiene follow-up remains.


Why MB696 mattered

MB696 was not a feature round. After MB695 had reconciled the long-lived database schema and established a trustworthy upgrade path, the next step toward a future stable release was deliberately less visible: review what could still hurt Mediabot from a security, secrets, TLS or repository-hygiene perspective.

The feature freeze therefore remained in force. The round concentrated on four main areas:

  • current and historical secret exposure;
  • outbound HTTPS/TLS verification;
  • executable security regression contracts;
  • Node.js / mbweb dependency hygiene.

No database schema change was introduced. No database write was required by the final validation. No Mediabot, Undernet or mbweb service restart was performed during the final proof.

This was a round about tightening doors rather than adding new rooms to the castle. 🏰


🔐 Current public source tree: clean

The first part of MB696 audited both the current public source tree and the full Git history with Gitleaks.

For the current tree, the result was clean:

Gitleaks findings: 0

The publication audit also reconfirmed that sensitive local material remains outside the public repository:

commit.sh       NOT TRACKED
mediabot.conf   NOT TRACKED
mbundernet.conf NOT TRACKED

No sensitive tracked filename was identified by the dedicated checks.

The important conclusion is simple:

The source code currently published by Mediabot does not contain a secret detected by Gitleaks.


🕰️ Four historical credential findings were classified

Scanning the complete Git history produced four findings. They are all concentrated in one historical location:

Rule   : generic-api-key
File   : Mediabot/Mediabot.pm
Commit : c59c27f702f8
Date   : 2023-08-07

The four matches correspond to two historical pairs of:

client_id
client_secret

The actual values were deliberately never printed during the review.

A sanitized inspection of the historical source identified the surrounding provider as the former Twitter API path:

api.twitter.com

This means these were not simply arbitrary words that happened to trigger a generic regular expression. They were credential-shaped historical assignments.

The safe operational position is therefore:

The old Twitter/X credentials must be considered historically exposed and should be confirmed revoked or otherwise unusable at the provider.

They do not exist in the current public source tree.

MB696 deliberately did not rewrite years of Git history merely to make a historical scanner counter disappear. A history rewrite would disrupt clones, forks and references while providing little security benefit if the historical provider credentials have already been invalidated.


🔒 TLS — retiring an old compatibility assumption

One of the most important findings was hidden in a piece of historical compatibility logic.

Mediabot centralizes many HTTP calls through:

Mediabot::External::_make_http(...)

Historically this factory defaulted to:

verify_SSL = 0

The source explicitly explained that behavior as an OVH/Kimsufi compatibility workaround.

In other words, TLS certificate verification was available, but the common factory disabled it unless a caller explicitly requested it.

Some sensitive API paths had already compensated by supplying:

verify_SSL => 1

but the global default itself remained insecure.

Before changing such an old compatibility rule, MB696 tested whether it was still actually required.


🔬 Testing the real TLS stack

The deployed host reported:

HTTP::Tiny      : 0.080
IO::Socket::SSL : 2.098
Net::SSLeay     : 1.92
HTTP can_ssl    : YES

Verified HTTPS requests were then tested against real public services without sending application credentials.

Successful paths included:

GitHub       HTTP 200
Google News  HTTP 200
country.is   HTTP 200
YouTube      HTTP 200
OpenAI       HTTP 401
Anthropic    HTTP 401

The OpenAI and Anthropic 401 responses are expected here. No credentials were intentionally supplied. Receiving an HTTP application response proves that the verified TLS/network layer completed successfully before authentication was rejected.


📰 The Google News 599 mystery

An initial Google News probe returned:

status=599

That initially looked like it might confirm the historical TLS problem. It did not.

A dedicated probe tested the same endpoint with verified and unverified TLS, using different response-size limits:

secure-64K
    status=599
    detail=Size of response body exceeds the maximum allowed of 65536

secure-512K
    status=200

insecure-64K
    status=599

insecure-512K
    status=200

The feed was approximately 123 KiB. The failure was therefore caused by the deliberately small 64 KiB test ceiling, not certificate verification.

OpenSSL independently confirmed the certificate chain:

Verification: OK
Verify return code: 0 (ok)

curl also completed successfully with HTTP 200.

The conclusion was clear:

The historical OVH/Kimsufi justification for disabling TLS verification globally is no longer valid on the current host.


🪄 Secure by default

MB696 therefore reverses the shared HTTP policy.

Before:

verify_SSL defaults to 0

After:

verify_SSL defaults to 1

The caller can still explicitly opt out where compatibility genuinely requires it. The important difference is that insecure TLS is no longer silent. It must now be visible in source code.

This changes the security model from:

remember to request certificate verification

into:

certificate verification is automatic unless somebody deliberately disables it.


🔑 Authenticated requests remain explicit

A secure common default is useful, but authenticated requests deserve an even clearer contract.

The Tavily news path carries an API credential. MB696 therefore gives that client an explicit:

verify_SSL => 1

even though the factory would already provide it by default.

That makes the requirement local and obvious. A future modification of the shared HTTP factory cannot silently weaken the authenticated Tavily path without also breaking its regression/security contract.


🌍 country.is no longer inherits HTTP::Tiny’s insecure default

The whereis implementation used a direct HTTP::Tiny->new(...) constructor rather than Mediabot’s common HTTP factory.

HTTP::Tiny itself does not provide the new Mediabot secure default. The direct HTTPS request therefore now explicitly uses:

verify_SSL => 1

The surrounding eval protection remains in place so a network error cannot unexpectedly propagate through the IRC command path.


📻 One reviewed compatibility exception remains

MB696 does not blindly pretend that every endpoint has the same operational constraints.

There remains one explicit production TLS opt-out in the configurable Icecast administration/radio client.

Final inventory:

Mediabot/AdminCommands.pm:198
PRODUCTION_TLS_BYPASS_COUNT=1

That exception is deliberately scoped.

The policy is now:

TLS verification by default everywhere. Any insecure compatibility exception must be explicit, narrow and reviewed.

That is fundamentally safer than making the entire application inherit the weakest endpoint’s requirements.


🛡️ The security audit now enforces the policy

tools/security_audit.pl was strengthened to match the new architecture.

Before MB696, it primarily checked that authenticated APIs explicitly enabled TLS even though the common factory was insecure by default.

After MB696, it validates stronger invariants:

  • _make_http() defaults to verified TLS;
  • explicit caller overrides still work;
  • unexpected production verify_SSL => 0 calls are rejected;
  • only the reviewed Icecast compatibility path may disable TLS verification;
  • Tavily’s authenticated request explicitly verifies TLS;
  • the direct country.is request verifies TLS;
  • authenticated Claude/API paths retain explicit TLS verification;
  • existing shell and argument-injection guards remain intact.

Final result:

Verdict: GO — all 14 security invariants hold.

🧪 Regression contracts were updated rather than deleted

Several older tests encoded the previous OVH/Kimsufi behavior.

They were not simply removed because the old architecture changed. Instead, their intent was preserved while the expected policy was updated.

Modified regression contracts include:

t/cases/138_helpers_getversion_no_curl_shell.t
t/cases/139_helpers_whereis_no_curl_process.t
t/cases/207_helpers_http_guard_regression.t
t/cases/820_mb640_version_check_never_silent.t

A dedicated MB696 TLS contract was added:

t/cases/909_mb696_tls_secure_default.t

It guards:

  • the secure factory default;
  • explicit compatibility opt-out support;
  • Tavily TLS verification;
  • country.is TLS verification;
  • the bounded Icecast exception;
  • the corresponding security-audit requirements.

📦 The forgotten Node vault

While reviewing security and release hygiene, MB696 also discovered something unrelated to Perl TLS:

526 files under root node_modules/ were tracked by Git

This deserved investigation rather than an immediate rm -rf.

History showed that the vendor tree had entered the repository in a single old mbweb commit:

8ae1c75
2026-04-27
🪄 Alohomora mbweb: unlock the first Mediabot v3 web console

The commit brought roughly 500 dependency files with the initial web console. They were never meaningfully maintained afterward.


The root npm manifests were not mbweb

The working tree also contained:

package.json
package-lock.json

at repository root.

They were not tracked and contained only:

express-rate-limit

That immediately looked suspicious because the actual mbweb application imports:

bcryptjs
dotenv
express
express-session
helmet
mysql2

The root npm state was therefore not the application dependency contract.


🏗️ Finding the real mbweb runtime

The source documentation and running service established the actual architecture.

Canonical source:

contrib/mbweb/

Live deployed application:

/opt/mbweb/app

systemd confirms:

WorkingDirectory=/opt/mbweb/app
ExecStart=/usr/bin/node /opt/mbweb/app/app.js

The real manifests are:

contrib/mbweb/package.json
contrib/mbweb/package-lock.json

They were compared with the deployed manifests:

/opt/mbweb/app/package.json
/opt/mbweb/app/package-lock.json

The SHA-256 values matched exactly for both files.

The canonical direct dependency contract is therefore known:

bcryptjs        ^2.4.3
dotenv          ^16.6.1
express         ^5.2.1
express-session ^1.19.0
helmet          ^8.0.0
mysql2          ^3.22.2

This finally separates three concepts that had historically become mixed together:

contrib/mbweb/       canonical project source
/opt/mbweb/app       live deployed runtime
root node_modules/   historical repository residue

📜 Canonical mbweb manifests are now tracked

The repository historically ignored all JSON files with:

*.json

That also accidentally hid npm manifests.

MB696 adds explicit exceptions for:

contrib/mbweb/package.json
contrib/mbweb/package-lock.json

Those two files are now included in the public repository.

A second dedicated test was added:

t/cases/910_mb696_node_dependency_hygiene.t

Its intended contract ensures that:

  • the canonical mbweb manifest is publishable;
  • the canonical lockfile is publishable;
  • manifest and lockfile dependency sets agree;
  • root npm residue is not accidentally published;
  • root node_modules/ must not remain part of the repository.

Focused validation:

2/2 files
19/19 tests
PASS

⚠️ One small follow-up remains after fd44628

Before commit, the root vendor tree was removed from the Git index without deleting its local files.

Proof:

Git node_modules before : 526
Git node_modules after  : 0

Local files before      : 640
Local files after       : 640

LOCAL_NODE_MODULES      : PRESERVED

The complete intended staged tree — including those 526 Git deletions — was then fully validated.

However, the local commit helper deliberately protects node_modules/.

During the actual commit it reported:

Protected paths were staged and have been removed from the commit

for the node_modules/** deletion entries.

The resulting commit summary was:

16 files changed, 1515 insertions(+), 54 deletions(-)

Therefore:

TLS/security changes       COMMITTED
mbweb manifests            COMMITTED
security tests             COMMITTED
security audit changes     COMMITTED

node_modules Git purge     VALIDATED, BUT NOT INCLUDED

This does not affect the live runtime. It is a repository-hygiene follow-up that must be completed before MB696 is marked completely closed.


✅ Validation proof

The intended MB696 final tree passed all major gates.

TLS/security targeted suite

12/12 files
145/145 tests
PASS

Dedicated MB696 contracts

2/2 files
19/19 tests
PASS

Fast validation lane

333/333 files
6137/6137 tests
PASS

Full test suite

793/793 files
16034/16034 tests
PASS

Security audit

Verdict: GO — all 14 security invariants hold.

Exact staged public-tree secret scan

GITLEAKS_FINDINGS=0
RC_GITLEAKS=0

🟢 Runtime remained untouched

The final validation observed unchanged processes:

DEV
3819132 -> 3819132

Undernet
3815619 -> 3815619

mbweb
228188 -> 228188

No database write occurred. No service restart occurred.

The security hardening was therefore validated without disturbing live application state.


📚 What landed in fd44628

.gitignore
CHANGELOG.md
VERSION

Mediabot/External.pm
Mediabot/External/News.pm
Mediabot/External/YouTube.pm
Mediabot/Helpers.pm

contrib/mbweb/package.json
contrib/mbweb/package-lock.json

t/cases/138_helpers_getversion_no_curl_shell.t
t/cases/139_helpers_whereis_no_curl_process.t
t/cases/207_helpers_http_guard_regression.t
t/cases/820_mb640_version_check_never_silent.t

t/cases/909_mb696_tls_secure_default.t
t/cases/910_mb696_node_dependency_hygiene.t

tools/security_audit.pl

Commit:

fd44628
🛡️ Ward the Network Gates and Clear the Forgotten Node Vault

Version:

3.4dev-20260823_142740

Push:

a191682..fd44628
master -> master

🧭 Where this leaves Mediabot

MB696 changes something more important than a visible IRC command.

It changes the direction of the project’s defaults.

Before:

secure HTTPS was possible, but callers sometimes had to remember to request it.

Now:

secure HTTPS is the normal behavior, and weakening it requires an explicit and reviewable decision.

Likewise, mbweb’s dependency model is no longer something that has to be reverse-engineered from an old node_modules directory.

The canonical dependency contract is identified, reproducible and represented by its proper manifest and lockfile.

There is one small repository cleanup still to seal because the local commit safety guard correctly refused to manipulate a protected path automatically.

Once that deliberate cleanup is landed, MB696 can be closed.

The castle did not learn a flashy new spell this time.

It got stronger locks, fewer forgotten keys, and a much clearer map of the hidden passages. 🛡️🗝️🪄


Next

Immediate:

finish the root node_modules Git-index cleanup
verify the historical Twitter/X credentials are revoked
close MB696

Then:

MB697 — runtime cleanup
MB698 — documentation and release contract
MB699 — final readiness report

A fresh Debian 13 manual verification remains useful but lower priority.

And, as before:

3.5 only happens after an explicit GO.

You must be logged in to reply.