The device token assumes a handful of fixed screens an administrator provisions once. That is not how these kiosks are used: most people open the page on their own PC, and there is no login, so per-browser provisioning would mean touching every PC on the floor and losing it again on the next reimage or cleared profile. A screen in that state did not look refused either - the USB kiosk rendered a working page whose every call answered 404. Both kiosks now admit a caller on either ground, the same shape the GE-Enforce client endpoints already use: the per-device token, or a source address inside an allowlist. Wall screens keep the token and are not confined to the allowlist; everyone else is admitted by where they are on the network. Still gated on the site's enable flag, so nothing opens by itself, and both allowlists ship empty - token-only until a site enters its ranges. - shopdb.api gains ip_in_cidrs and request_ip_allowlisted (contract 0.35.0). On the surface because both plugins need the identical gate, and a CIDR parser copied into two of them is where a fail-open creeps in later. They fail closed on an empty setting, an unparseable caller address and a malformed entry, and they read remote_addr rather than raw X-Forwarded-For - so the IIS rewrite rule is what makes them meaningful, which the module says at length. - Separate settings per kiosk, usb_kiosk_allowed_cidrs and printedparts_kiosk_allowed_cidrs. One must not grant the other, for the same reason the tokens are separate. - Both settings pages get a picker listing the network plugin's subnets with their VLANs. It stores resolved CIDR strings, not subnet ids: the setting has to work at a site that does not run that plugin and must not break when an inventory row is deleted. Entries matching no subnet are shown rather than hidden, since a stale one left by an inventory edit looks exactly like a hand-typed range. - USBKiosk says it was not admitted instead of pretending to work, which PartsKiosk already did. - The USB settings page keyed text-vs-boolean off the token alone. Adding a second text setting to the boolean path would have saved it as the word 'true' - the bug that replaced the kiosk token and 404'd every provisioned screen. Both load and save now key off a list, with a spec on it. The trade, stated plainly in the admin UI: anyone on an allowed network can check a drive in or out against a badge number they guess, and the record carries that person's name. Badges are attribution, not authorization. Keeping the ranges narrow is what bounds it; login is the alternative and was ruled out because these users do not sign in.
65 lines
2.7 KiB
Python
65 lines
2.7 KiB
Python
"""Network-position allowlists for unauthenticated kiosk routes.
|
|
|
|
A kiosk route writes without a login. The device token (see the kiosk views)
|
|
covers unattended screens an administrator provisions once, but it does not fit
|
|
the other caller: somebody opening the kiosk page on their own PC, who never
|
|
logs in and whose browser nobody provisioned. Per-browser provisioning would
|
|
mean touching every PC on the floor and losing it again on the next reimage.
|
|
What those callers have in common is where they are on the network.
|
|
|
|
Fails closed, in every direction that matters:
|
|
- an empty or absent setting matches nothing (the feature is off),
|
|
- an unparseable caller address matches nothing,
|
|
- a malformed entry in the list is skipped rather than widening the match.
|
|
|
|
SPOOF RESISTANCE IS A DEPLOYMENT PROPERTY, NOT A PROPERTY OF THIS CODE.
|
|
The caller address comes from client_ip(), which reads remote_addr and never
|
|
the raw X-Forwarded-For header - reading that header directly would let any
|
|
caller send 'X-Forwarded-For: <allowlisted-ip>' and walk straight through.
|
|
remote_addr is only the real client because of what sits in front of the app:
|
|
behind IIS the URL-Rewrite rule overwrites X-Forwarded-For with the real TCP
|
|
peer and ProxyFix resolves it into remote_addr (TRUSTED_PROXY_COUNT). Remove
|
|
that rule and IIS forwards whatever the caller sent; waitress trusts it because
|
|
it arrives from 127.0.0.1, remote_addr becomes attacker-controlled, and every
|
|
allowlist built on this becomes bypassable from anywhere on the network. It
|
|
does NOT fail closed in that case. The rewrite rule is the control. The
|
|
GE-Enforce client allowlist carries the same dependency.
|
|
|
|
Callers store plain CIDR strings rather than references to subnet records: a
|
|
site that does not run the network plugin still has to be able to configure
|
|
this, and a stored allowlist must not break when an inventory row is deleted.
|
|
"""
|
|
|
|
import ipaddress
|
|
|
|
from shopdb.utils.clientip import client_ip
|
|
|
|
|
|
def ip_in_cidrs(address, rawcidrs):
|
|
"""True when `address` falls inside any CIDR in the comma-separated list.
|
|
|
|
Pure and side-effect free so it can be tested without a request context.
|
|
"""
|
|
raw = (rawcidrs or '').strip()
|
|
if not raw:
|
|
return False
|
|
try:
|
|
caller = ipaddress.ip_address((address or '').strip())
|
|
except ValueError:
|
|
return False
|
|
for part in raw.split(','):
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
try:
|
|
if caller in ipaddress.ip_network(part, strict=False):
|
|
return True
|
|
except ValueError:
|
|
continue
|
|
return False
|
|
|
|
|
|
def request_ip_allowlisted(request_obj, rawcidrs):
|
|
"""True when the request's client address is inside the allowlist."""
|
|
return ip_in_cidrs(client_ip(request_obj), rawcidrs)
|