From 8a99544ee19567fe04b9a2cd3bf4ac39fdeec887 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 14:40:33 +0000 Subject: [PATCH] Close remaining 10-vs-11-digit gaps in PSTN number input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full audit of every phone-number comparison/storage point across pstn-trunk.sh, security-dashboard.sh, asterisk.sh, asterisk-digital-ocean.sh, and the vendored easy-asterisk base script, prompted by the inbound Caller-ID normalization fix — the same digit-count mismatch was also possible on the admin-input side, just silent instead of loud: - security-dashboard.sh's write_permission(): a 10-digit whitelist entry was silently DROPPED (NUMBER_RE required exactly 11 digits), with no warning unless every entry in the field was invalid — a mixed 10-digit + 11-digit list saved "successfully" while quietly losing the 10-digit one. Now normalizes any bare 10-digit token to 11-digit instead of discarding it (_normalize_nanp_number). - write_personal_did(): required exactly 10 digits, rejecting an 11-digit entry outright with a clear (but avoidable) error. Now accepts either and normalizes to the canonical 10-digit storage form (_normalize_personal_did_input). - pstn-trunk.sh's TRUNK_DID install prompt: same fix, strips a leading "1" instead of aborting the install over it. Everything else checked out clean: outbound dialed-number matching already normalizes via the existing _NXXNXXXXXX pattern (adds "1" before any tier check), the area-code/country-code international gates aren't phone numbers so digit-count doesn't apply, and asterisk.sh/ asterisk-digital-ocean.sh/the vendored base script have no phone-number comparison logic at all — this class of bug only lives in the PSTN permission/whitelist layer this project added on top. --- services/pstn-trunk.sh | 7 +++++ services/security-dashboard.sh | 52 ++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index 497af29..e7465bc 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -1666,6 +1666,13 @@ install_pstn-trunk() { local TRUNK_DID="" prompt_text "DID (the 10-digit US phone number assigned to this trunk, digits only):" "" TRUNK_DID + # Accept an 11-digit entry (leading "1") too and strip it — every other + # DID/number field in this system (personal_did, allowed_numbers) has + # the same 10-vs-11-digit ambiguity, and rejecting a plainly-valid + # 11-digit number here just to force a re-prompt is needless friction. + if [[ "$TRUNK_DID" =~ ^1([0-9]{10})$ ]]; then + TRUNK_DID="${BASH_REMATCH[1]}" + fi if [[ ! "$TRUNK_DID" =~ ^[0-9]{10}$ ]]; then log_error "That doesn't look like a 10-digit US number — aborting." return 1 diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 4369bd8..c2a57a1 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -782,6 +782,23 @@ EXT_HEADER_RE = re.compile(r"^\[(\d+)\]") EXTEN_RE = re.compile(r"^\d+$") TIER_RE = re.compile(r"^(internal|restricted|full)$") NUMBER_RE = re.compile(r"^\d{11}$") +NUMBER_RE_10 = re.compile(r"^\d{10}$") + + +def _normalize_nanp_number(token): + """Accepts either a bare 10-digit NANP number (the natural way to type + a US number) or an already-11-digit one (leading "1" country code) and + returns the canonical 11-digit form allowed_numbers is always stored + in - REGEX() comparisons against CALLERID(num) require an exact + digit-count match, and this used to silently DROP a plain 10-digit + entry instead of normalizing it, the admin-input-side twin of the bug + that was also failing inbound calls whose Caller-ID itself arrived + without a leading "1" (see PSTN_CALLERID_NORM in pstn-trunk.sh).""" + if NUMBER_RE_10.match(token): + return "1" + token + if NUMBER_RE.match(token): + return token + return None SECURITY_LOG_TAIL_BYTES = 2 * 1024 * 1024 # comfortably enough for 5000 lines @@ -1149,11 +1166,13 @@ def write_permission(ext, tier, numbers_raw, messaging_enabled=False): the calling tier (see pstn-trunk.sh's file-level comment: an extension can be internal-tier for calling and still messaging-enabled, or vice versa), so it's set/cleared regardless of which tier branch runs below. - Numbers are normalized to a pipe-separated list of 11-digit US numbers — - pipe, not comma, because the dialplan uses this value directly as a - REGEX() alternation pattern (see services/pstn-trunk.sh's file-level - comment on why the untrusted call data is always the string being - tested, never interpolated into the pattern side).""" + Numbers are normalized to a pipe-separated list of 11-digit US numbers + (a bare 10-digit entry gets a leading "1" added, not dropped — see + _normalize_nanp_number) — pipe, not comma, because the dialplan uses + this value directly as a REGEX() alternation pattern (see + services/pstn-trunk.sh's file-level comment on why the untrusted call + data is always the string being tested, never interpolated into the + pattern side).""" if not ASTERISK_CONFIG_DIR: return False, "No Asterisk install detected on this box" ext = str(ext).strip() @@ -1163,7 +1182,7 @@ def write_permission(ext, tier, numbers_raw, messaging_enabled=False): return False, "Invalid tier" tokens = re.split(r"[,\s|]+", (numbers_raw or "").strip()) - clean_numbers = [t for t in tokens if NUMBER_RE.match(t)] + clean_numbers = [n for n in (_normalize_nanp_number(t) for t in tokens if t) if n] numbers = "|".join(clean_numbers) cp = _read_permissions_cp() @@ -1405,6 +1424,19 @@ def write_limits(max_outbound, max_inbound): PERSONAL_DID_RE = re.compile(r"^\d{10}$") +PERSONAL_DID_RE_11 = re.compile(r"^1\d{10}$") + + +def _normalize_personal_did_input(did): + """write_personal_did()'s DID field is hand-typed, same footgun as + allowed_numbers - accept either the canonical bare 10-digit form or an + 11-digit one with the NANP "1" prefix, returning the canonical 10-digit + form either way instead of rejecting a plainly-valid 11-digit entry.""" + if PERSONAL_DID_RE.match(did): + return did + if PERSONAL_DID_RE_11.match(did): + return did[1:] + return None def _personal_dids_path(): @@ -1460,8 +1492,10 @@ def write_personal_did(did, owner): return False, "No Asterisk install detected on this box" did = str(did).strip() owner = str(owner).strip() - if not PERSONAL_DID_RE.match(did): - return False, "DID must be a 10-digit US number" + norm_did = _normalize_personal_did_input(did) + if norm_did is None: + return False, "DID must be a 10-digit US number (11-digit with a leading 1 also accepted)" + did = norm_did is_group = owner.startswith("@") group_name = owner[1:] if is_group else "" @@ -3154,7 +3188,7 @@ function renderPstnTable() { - + `).join("");