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.
93 lines
3.4 KiB
Python
93 lines
3.4 KiB
Python
"""How the parts kiosk decides a caller is admitted by network position.
|
|
|
|
The USB kiosk closed this on 2026-09-18 and this one did not, so the two read
|
|
differently for no reason anybody chose. The hole is the same one: a reverse
|
|
proxy normally sits ON the network a site enters in the allowlist, so a request
|
|
wearing the proxy's own address matched the range and was admitted while
|
|
carrying no evidence at all about who sent it.
|
|
|
|
Shape of the requests here matches production: the layer in front (IIS with the
|
|
X-Forwarded-For rewrite, or nginx plus ProxyFix) has already resolved the caller
|
|
into REMOTE_ADDR, and the app never reads the header itself.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from shopdb.core.models import Setting
|
|
|
|
|
|
PLANT = '10.134.48.17'
|
|
PLANT_CIDR = '10.134.48.0/23'
|
|
ELSEWHERE = '10.9.100.5'
|
|
# Inside the allowlisted range, which is where a proxy actually lives and the
|
|
# reason the un-subtracted check was wrong.
|
|
PROXY = '10.134.48.5'
|
|
PROXY_CIDR = '10.134.48.5/32'
|
|
KIOSK_TOKEN = 'parts-kiosk-admission-token'
|
|
|
|
|
|
def _set(key, value):
|
|
Setting.set(key, value, valuetype='string', category='printedparts')
|
|
|
|
|
|
@pytest.fixture
|
|
def kiosk(app, db):
|
|
_set('printedparts_kiosk_enabled', 'true')
|
|
_set('printedparts_kiosk_token', KIOSK_TOKEN)
|
|
_set('printedparts_kiosk_allowed_cidrs', '')
|
|
_set('printedparts_kiosk_trusted_proxies', '')
|
|
return app
|
|
|
|
|
|
def _open_from(app, address):
|
|
"""Ask the gate directly: it is the unit under test, not a route's shape.
|
|
|
|
The request context comes from the REAL app, so Setting.get reaches the same
|
|
database the fixture wrote to.
|
|
"""
|
|
from plugins.printedparts.api.routes import _kiosk_open
|
|
|
|
with app.test_request_context('/', environ_base={'REMOTE_ADDR': address}):
|
|
return _kiosk_open()
|
|
|
|
|
|
def test_a_caller_on_an_allowed_network_is_admitted(kiosk):
|
|
_set('printedparts_kiosk_allowed_cidrs', PLANT_CIDR)
|
|
assert _open_from(kiosk, PLANT) is True
|
|
|
|
|
|
def test_a_caller_outside_every_allowed_network_is_refused(kiosk):
|
|
_set('printedparts_kiosk_allowed_cidrs', PLANT_CIDR)
|
|
assert _open_from(kiosk, ELSEWHERE) is False
|
|
|
|
|
|
def test_a_request_wearing_the_proxy_address_fails_closed(kiosk):
|
|
"""THE FIX. The proxy forwarded nothing, so its own address arrived - and it
|
|
sits inside the allowlist. No evidence about the sender means refuse."""
|
|
_set('printedparts_kiosk_allowed_cidrs', PLANT_CIDR)
|
|
_set('printedparts_kiosk_trusted_proxies', PROXY_CIDR)
|
|
assert _open_from(kiosk, PROXY) is False
|
|
# ... and the feature still works for a real caller on the same network, or
|
|
# the subtraction has taken the allowlist with it.
|
|
assert _open_from(kiosk, PLANT) is True
|
|
|
|
|
|
def test_loopback_is_never_a_kiosk_client(kiosk):
|
|
"""Needs no configuration: no site's kiosk is the server itself. This is
|
|
also the shape a misconfigured front end produces, where every request
|
|
arrives as 127.0.0.1."""
|
|
_set('printedparts_kiosk_allowed_cidrs', '127.0.0.0/8, ' + PLANT_CIDR)
|
|
assert _open_from(kiosk, '127.0.0.1') is False
|
|
|
|
|
|
def test_the_kiosk_flag_still_governs(kiosk):
|
|
"""Network position admits nobody while the kiosk is off."""
|
|
_set('printedparts_kiosk_allowed_cidrs', PLANT_CIDR)
|
|
_set('printedparts_kiosk_enabled', 'false')
|
|
assert _open_from(kiosk, PLANT) is False
|
|
|
|
|
|
def test_an_empty_allowlist_admits_nobody(kiosk):
|
|
"""Token-only, which is the shipped default."""
|
|
assert _open_from(kiosk, PLANT) is False
|