fix(logo): don't let a non-SVG / proxy error response break the logo route

get_logo_svg() cached whatever LOGO_URL returned without validating it, and get_logo() then ran str.format() over it. Any literal '{' in the payload - an HTML error page from a proxy, captive portal or CDN, e.g. a Cloudflare 523 page whose CSS uses braces - raised ValueError: unexpected '{' in field name and broke the route for every request until restart.

Validate that the fetched markup is SVG, fall back to the bundled web/common/media/rgthree.svg, substitute {bg}/{fg} with str.replace instead of str.format, and never serve non-SVG content from the route.
This commit is contained in:
ussoewwin
2026-09-14 20:57:18 +09:00
parent 2c5342a8cb
commit 17f184820b
2 changed files with 28 additions and 3 deletions
+22 -2
View File
@@ -45,6 +45,19 @@ if not LOGO_URL.endswith('.svg'):
raise ValueError('Bad logo url.')
LOGO_SVG = None
def _local_logo_svg():
"""The logo bundled with this repository, used when the remote one is unavailable."""
import os
try:
path = os.path.join(_THIS_DIR, '..', 'web', 'common', 'media', 'rgthree.svg')
with open(path, 'r', encoding='utf-8') as f:
return f.read()
except Exception:
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"></svg>'
async def get_logo_svg():
import aiohttp
global LOGO_SVG
@@ -62,9 +75,16 @@ async def get_logo_svg():
'Expires': '0'
}
async with session.get(LOGO_URL, headers=headers) as resp:
LOGO_SVG = await resp.text()
fetched = await resp.text()
# The response is not guaranteed to be our SVG: a proxy, a captive portal or a
# CDN error page answers with HTML (Cloudflare pages contain `{` in their CSS,
# which later blows up `str.format` in the route). Only accept real SVG markup
# and otherwise fall back to the bundled logo below.
if '<svg' not in fetched.lower():
raise ValueError('remote logo is not SVG')
LOGO_SVG = fetched
LOGO_SVG = re.sub(r'(id="bg".*fill=)"[^\"]+"', r'\1"{bg}"', LOGO_SVG)
LOGO_SVG = re.sub(r'(id="fg".*fill=)"[^\"]+"', r'\1"{fg}"', LOGO_SVG)
except Exception:
LOGO_SVG = '<svg></svg>'
LOGO_SVG = _local_logo_svg()
return LOGO_SVG
+6 -1
View File
@@ -45,7 +45,12 @@ async def get_logo(request, as_markup=False):
h = get_param(request, 'h')
css_class = get_param(request, 'cssClass')
svg = await get_logo_svg()
resp = svg.format(bg=bg, fg=fg)
# `str.format` raises on any stray `{` in the markup, which turns a cached
# non-SVG response into a 500 for the whole route. Substitute the two
# placeholders directly, and never serve non-SVG content.
if '<svg' not in svg.lower():
svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"></svg>'
resp = svg.replace('{bg}', bg).replace('{fg}', fg)
if w is not None:
resp = re.sub(r'(<svg[^\>]*?)width="[^\"]+"', r'\1', resp)
if str(w).isnumeric():