Skip to content

v0.11.7 — AdSense revenue panel, and four holes closed in the running system

Choose a tag to compare

@HaoweiChan HaoweiChan released this 21 Aug 06:48
· 50 commits to main since this release
1ce18ed

Patch release. Adds the monetization half of the admin analytics page, and closes four exposures found by auditing the running VPS rather than by reading code. Nothing user-visible changes — that is why there is no what's-new entry this time.

Security

marp-flask-service was on the public internet (#536)

The compose file published 5004:5004, which binds 0.0.0.0. Docker inserts its publish rules ahead of ufw, so the host's default deny (incoming) never applied — unlike every other container here, all of which bind 127.0.0.1. Verified from outside the host, not inferred: an unauthenticated POST /convert returned a rendered PPTX.

That endpoint and /render-png feed arbitrary markdown to marp, which renders it with headless Chromium and --allow-local-files; /upload writes arbitrary request bodies to disk. There is no auth, no TLS and no rate limit, and each request spawns a Chromium with a 30–120s timeout. Now bound to loopback — the only caller is social_cards_render.py on the same host, whose MARP_SERVICE_URL already defaults to http://localhost:5004.

API docs and schema no longer served in production (#537)

/docs and /openapi.json were public on both api.tinboker.com and podcast-api.tinboker.com — a complete map of every endpoint and parameter, including the key-gated write routes on the pipelines app. The backend gates them on settings.is_production, so dev and staging keep theirs; podcast-api is blocked in Caddy, since that app is a single hand-deployed instance with no env split of its own.

podcast-api read endpoints are no longer world-readable (#539, #540)

1,441 enriched news articles, episode claims and ticker insights were free to scrape. Three months of access log confirmed no external consumer exists before anything was locked: the frontend never references that host, the main backend does not proxy it, and 333 of 431 /api requests were 404s from scanners probing /api/.env and friends.

/api/* now answers only to localhost and private ranges. /media/* stays public — those are article covers served to real browsers, Googlebot-Image, facebookexternalhit and the cover pipeline.

Worth recording: #539 first gated this on the presence of X-API-Key, assuming the app validated the value. Only the write routes do — curl -H 'X-API-Key: anything' passed straight through, and since this repo is public that commit's own comment published the bypass. #540 replaces it with the network-scope gate, which needs no shared secret and fails closed.

AdSense crawler let through the meta middleware (#538)

mediapartners added to the crawler pattern in the Cloudflare Pages middleware.

Admin

AdSense revenue panel (#535)

A new card on the admin analytics page: estimated earnings, page RPM, ad-request coverage (fill rate) and Active-View viewability, plus a per-day series and the top earning pages. Coverage and viewability are the actionable pair — coverage drops mean ad requests are not being filled, low viewability means Auto ads landed below the fold.

AdSense has no service-account support, so this reads a stored authorized-user OAuth credential (ADSENSE_OAUTH_JSON) and falls back to local ADC on a dev box; every failure path returns configured/available flags instead of raising, matching CloudflareAnalyticsService.

tinboker.com is still GETTING_READY in AdSense review, so the card shows an explicit review banner rather than a bare row of zeros that would read as broken serving.

Also in this release

3c694ff (NaN-safe sector performance sums) reached develop via the back-merge in #541. It is the fix that stopped the /api/sectors/performance 500s in production — last one at 08-19 18:04 UTC, none since.

Full changelog: v0.11.6...v0.11.7