Refuse a kiosk caller wearing an address that belongs to the path
Both kiosks admit a check-in or check-out with no login when the caller sits in a configured network. The subtraction that makes that safe was missing. A reverse proxy normally sits ON the plant network a site enters in the allowlist. When the proxy forwards nothing - misconfigured, or something bypassed it - the request arrives wearing the proxy's own address, which matches the allowlist, while carrying no evidence whatever about who sent it. That is the one case that has to refuse, and it was being admitted. The previous fail-closed test only looked closed because the proxy in it happened to sit outside the range. Addresses that belong to the path are now subtracted before the allowlist is consulted: loopback always, since no site's kiosk is the server itself, plus whatever the site declares as its proxies. USB and printedparts get the same treatment - they had diverged for no reason anybody chose. Three more, same surface: A range that admits everybody can no longer be saved by accident. 0.0.0.0/0 and ::/0 are refused outright, anything wider than a site plausibly needs is refused unless an explicit override is set, and the refusal says why. The kiosk token no longer travels in a URL. The provisioning link carries a single-use setup code redeemed on first open, so the token stays out of browser history, referrers and proxy logs. The route is declared public deliberately: the secret is the code, the way a password-reset link is the secret. The badge-to-name lookup is bounded. It answers without a login and is now reachable from a whole network rather than one screen, so a source that keeps missing gets refused rather than being allowed to walk the directory. On by default, budget and kill switch are settings. All four new settings carry controls on their settings pages. A setting that can stop the kiosk, or widen it, must not be reachable only by hand-editing rows. The installer's client-IP advice is corrected: it described the opposite of what the shipped waitress flags do, and named only GE-Enforce as the thing that breaks when the rule is absent, when the two kiosk allowlists depend on it too.
This commit is contained in:
@@ -80,7 +80,7 @@ import re
|
||||
from flask import current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from shopdb.api import Setting
|
||||
from shopdb.api import Setting, client_ip, ip_in_cidrs
|
||||
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp'}
|
||||
IMAGE_URL_PREFIX = '/api/printedparts/image/'
|
||||
@@ -518,12 +518,36 @@ def adjust_item(item_id: int):
|
||||
|
||||
import hmac
|
||||
|
||||
from shopdb.api import request_ip_allowlisted, audit_log
|
||||
from shopdb.api import audit_log
|
||||
|
||||
# Largest single take the kiosk records. A shop-floor grab is single digits; 25
|
||||
# leaves real use untouched and denies draining a bin in one request.
|
||||
KIOSK_MAX_TAKE_QUANTITY = 25
|
||||
|
||||
# Addresses that belong to the PATH, not to a caller. Subtracted from the
|
||||
# allowlist before it is consulted.
|
||||
#
|
||||
# WHY, and it is the half that was missing here: a reverse proxy normally sits
|
||||
# ON the network a site enters in printedparts_kiosk_allowed_cidrs. A request
|
||||
# that arrives wearing the proxy's own address - because the proxy forwarded
|
||||
# nothing, or because nothing forwarded at all - therefore MATCHED the allowlist
|
||||
# and was admitted while carrying no evidence about who actually sent it. Being
|
||||
# inside the allowlist is not enough; the address also has to be one a client
|
||||
# could plausibly have had.
|
||||
#
|
||||
# Loopback is never a client at any site, so it needs no configuration. The USB
|
||||
# kiosk closed this on 2026-09-18 and this one did not, which is the only reason
|
||||
# the two read differently.
|
||||
PARTS_KIOSK_TRUSTED_PROXIES = 'printedparts_kiosk_trusted_proxies'
|
||||
PARTS_KIOSK_NEVER_A_CLIENT = '127.0.0.0/8, ::1/128'
|
||||
|
||||
|
||||
def _kiosk_address_is_infrastructure(address):
|
||||
"""True when this address belongs to the path rather than to a caller."""
|
||||
if ip_in_cidrs(address, PARTS_KIOSK_NEVER_A_CLIENT):
|
||||
return True
|
||||
return ip_in_cidrs(address, Setting.get(PARTS_KIOSK_TRUSTED_PROXIES))
|
||||
|
||||
|
||||
def _kiosk_token_presented():
|
||||
"""True when the caller carries this kiosk's device token."""
|
||||
@@ -553,8 +577,10 @@ def _kiosk_open():
|
||||
return False
|
||||
if _kiosk_token_presented():
|
||||
return True
|
||||
return request_ip_allowlisted(
|
||||
request, Setting.get('printedparts_kiosk_allowed_cidrs'))
|
||||
address = client_ip(request)
|
||||
if _kiosk_address_is_infrastructure(address):
|
||||
return False
|
||||
return ip_in_cidrs(address, Setting.get('printedparts_kiosk_allowed_cidrs'))
|
||||
|
||||
|
||||
@printedparts_bp.route('/kiosk/provisioning-link', methods=['GET'])
|
||||
|
||||
Reference in New Issue
Block a user