fix(screenshots): validate cached screenshot image bytes on read and write#42120
Open
eschutho wants to merge 1 commit into
Open
fix(screenshots): validate cached screenshot image bytes on read and write#42120eschutho wants to merge 1 commit into
eschutho wants to merge 1 commit into
Conversation
…write Reject stale/empty/corrupt cached screenshot payloads instead of serving them: get_from_cache_key now treats a payload that claims a successful screenshot but has empty or non-image bytes as a cache miss, and compute_and_cache applies the same cheap header check before marking a result as cached-success, falling back to ERROR status otherwise. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Decisions made that were not in the instructions
\x89PNG\r\n\x1a\n/\xff\xd8\xff), not a full image decode, per the instruction to keep this cheap. A full decode would add CPU cost to every cache read/write for a class of corruption (truncated/blank-but-headered files) that a header check won't catch — deemed out of scope for closing the specific "blank PDF from stale cache" report.superset/charts/api.py. Both the dashboard and chartscreenshot()/thumbnail()/cache_*handlers already funnel through the sharedBaseScreenshot.get_from_cache_key()classmethod, so fixing validation there closes the read-side hole for both resource types mechanically, with no separate chart-specific patch.get_from_cache_key()rather than toScreenshotCachePayload.get_image()or__init__. This scopes the change to the cache-retrieval choke point (what the endpoints actually call to decide "hit vs. miss") without changing the behavior ofget_image()/ScreenshotCachePayloadconstruction elsewhere (e.g. the Celery report/alert pipeline, which callsget_screenshot()directly and never touches this cache layer).SUMMARY
Dashboard/chart screenshot and thumbnail caching (
ScreenshotCachePayloadinsuperset/utils/screenshots.py) had two gaps:BaseScreenshot.get_from_cache_key()returned whatever was in the cache as long as a status ofUPDATEDwas recorded, even if the stored image wasNone/0-byte or otherwise not a real image. Since the cache key is digest-based, an unchanged dashboard/chart kept serving the same stale/blank entry indefinitely (e.g. a blank PDF download).ERRORstatus when the screenshot task produced a falsy (None/b"") result, but a non-empty, non-image payload (e.g. truncated/corrupt bytes) still passed theif image:check and got cached withUPDATEDstatus.This PR adds a shared, cheap validator (
validate_screenshot_image()— checks non-empty + PNG/JPEG magic-byte header, no full decode) used on both paths:get_from_cache_key()now rejects a cached payload that claims a successful screenshot (status == UPDATED) but fails validation, returningNone— the same value callers already treat as a cache miss — and logs aWARNINGwith the cache key and the reason (emptyvsundecodable). Because both the dashboard and chartscreenshot/thumbnail/cache_*endpoints already call this same shared classmethod, this closes the hole for both resource types without touchingcharts/api.pyordashboards/api.py.BaseScreenshot.compute_and_cache()now runs the same check on the freshly generated image before caching it as a success. If it fails, the payload is markedERROR(consistent with fix(screenshots): catch empty-bytes tiled result and set ERROR on falsy image #41097's approach) instead ofUPDATED, and aWARNINGwith the cache key and reason is logged.Explicitly not changed:
superset/utils/screenshot_utils.py,superset/utils/webdriver.py) — untouched, owned by concurrent PRs fix(reports): positive per-tile chart readiness check for tiled screenshots #42119 and fix(reports): time-budget tiled screenshot to fail cleanly instead of hitting Celery kill #42118.TESTING INSTRUCTIONS
Added unit tests in
tests/unit_tests/utils/test_screenshot_cache_fix.pyand updated fixtures intests/unit_tests/utils/screenshot_test.py(existing tests used non-image placeholder bytes likeb"image_data"as stand-ins for a "successful" screenshot; these now use a minimal valid PNG header so they still exercise the success path under the new validation):None) with aWARNINGlogged; a payload with non-image garbage bytes is likewise treated as a cache miss; a valid PNG-header payload is served normally; a non-UPDATED(e.g.PENDING) payload is returned as-is.compute_and_cache()with an empty or garbage-bytes screenshot result cachesERRORstatus (neverUPDATED) and logs aWARNINGincluding the cache key and reason.Run:
48 passed. Also ran the broader
tests/unit_tests/utils/ -k screenshotsuite (76 passed); 3 unrelated pre-existing failures inwebdriver_test.py/test_screenshot_utils.pyreproduce identically onmaster(playwright-version mismatch in the test environment, unrelated to this change and in files this PR does not touch).ruff checkpasses on the changed files;mypyreports no errors insuperset/utils/screenshots.py.ADDITIONAL INFORMATION
🤖 Generated with Claude Code