Remediate the 2026-09-03 feature-test findings
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import glob
|
||||
import hashlib
|
||||
import re
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, current_app
|
||||
@@ -20,6 +21,7 @@ from shopdb.utils.responses import (
|
||||
)
|
||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.datevalues import DateValueError, parse_date
|
||||
|
||||
|
||||
# Uploads live in the instance dir, like model images and part photos, so a
|
||||
@@ -326,6 +328,24 @@ def create_version(app_id: int):
|
||||
if not data or not data.get('version'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'version is required')
|
||||
|
||||
# A digest is a claim the delivery path CHECKS against, so a wrong one is
|
||||
# worse than none: an over-long value was silently truncated to the column
|
||||
# width and came back as 64 plausible hex characters, indistinguishable
|
||||
# from a real SHA-256. Either it is a SHA-256 or it is refused.
|
||||
digest = (data.get('sha256') or '').strip().lower()
|
||||
if digest and not re.fullmatch(r'[0-9a-f]{64}', digest):
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'sha256 must be 64 hexadecimal characters')
|
||||
|
||||
# Refused rather than stored raw: a malformed date used to reach MySQL,
|
||||
# which (before STRICT_TRANS_TABLES) wrote 0000-00-00 and then broke every
|
||||
# read of this application.
|
||||
try:
|
||||
releasedate = parse_date(data.get('releasedate'), 'releasedate')
|
||||
except DateValueError as bad:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, str(bad))
|
||||
|
||||
if AppVersion.query.filter_by(appid=app_id, version=data['version']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
@@ -336,9 +356,9 @@ def create_version(app_id: int):
|
||||
version = AppVersion(
|
||||
appid=app_id,
|
||||
version=data['version'],
|
||||
releasedate=data.get('releasedate'),
|
||||
releasedate=releasedate,
|
||||
notes=data.get('notes'),
|
||||
sha256=data.get('sha256'),
|
||||
sha256=digest or None,
|
||||
silentargs=data.get('silentargs')
|
||||
)
|
||||
|
||||
@@ -558,9 +578,20 @@ def upload_application_image(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/image/<path:filename>', methods=['GET'])
|
||||
@require_permission('applications.view')
|
||||
def serve_application_image(filename):
|
||||
"""Serve an uploaded application image (public - lists and tiles read it)."""
|
||||
"""Serve an uploaded application image.
|
||||
|
||||
Ungated, like every other image route in the product (the branding logo,
|
||||
the map blueprint, an employee photo, a printed-part picture). A browser
|
||||
fetching <img src="/api/applications/image/application-3.png"> sends no
|
||||
Authorization header, so a permission check here does not protect the file
|
||||
- it just guarantees the picture never appears. The tile fell back to its
|
||||
broken-image handler on every page, including for an admin.
|
||||
|
||||
What is exposed is an application icon to somebody who already knows the
|
||||
filename, which is the same posture as its four siblings. Registered in
|
||||
tests/test_core/test_public_read_surface.py with that reasoning.
|
||||
"""
|
||||
return send_upload(_appimage_dir(), filename)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user