From b5ae4a566874f6bb159aa42650ff69d9a2ed63b0b23f86fd6088504bdbf0a026 Mon Sep 17 00:00:00 2001 From: ahdoawhfo Date: Fri, 17 Apr 2026 10:38:23 +0800 Subject: [PATCH] Harden IPv6 validation --- lib/common.sh | 60 ++++++++++++++++++++++++++++++++++++++++---- tests/test_common.sh | 4 +++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 53bd30b..83832c2 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -247,13 +247,42 @@ validate_ipv4() { done } +_validate_ipv6_part_count() { + local part=${1-} + local IFS=':' + local -a fields=() + local field idx count=0 + + [[ -n ${part} ]] || { + printf '0\n' + return 0 + } + + [[ ${part} != :* ]] || return 1 + [[ ${part} != *: ]] || return 1 + [[ ${part} != *::* ]] || return 1 + + read -r -a fields <<<"${part}" + for idx in "${!fields[@]}"; do + field=${fields[idx]} + [[ -n ${field} ]] || return 1 + if [[ ${field} == IPV4TAIL ]]; then + (( idx == ${#fields[@]} - 1 )) || return 1 + ((count += 2)) + continue + fi + [[ ${field} =~ ^[0-9A-Fa-f]{1,4}$ ]] || return 1 + ((count++)) + done + + printf '%s\n' "${count}" +} + validate_ipv6() { local ip=${1-} - local base scope + local base scope tail prefix left right left_count right_count total_count [[ -n ${ip} ]] || return 1 - [[ ${ip} != *.* ]] || return 1 [[ ${ip} == *:* ]] || return 1 - [[ ${ip} != *:::* ]] || return 1 scope='' base=${ip} @@ -264,8 +293,29 @@ validate_ipv6() { [[ ${scope} =~ ^[A-Za-z0-9_.:-]+$ ]] || return 1 fi - [[ ${base} =~ ^[0-9A-Fa-f:]+$ ]] || return 1 - [[ ${base} =~ :: ]] || [[ ${base} =~ ^([0-9A-Fa-f]{1,4}:){1,7}[0-9A-Fa-f]{1,4}$ ]] || return 1 + if [[ ${base} == *.* ]]; then + tail=${base##*:} + prefix=${base%:*} + [[ ${prefix} != "${base}" ]] || return 1 + validate_ipv4 "${tail}" || return 1 + base="${prefix}:IPV4TAIL" + fi + + [[ ${base//IPV4TAIL/} =~ ^[0-9A-Fa-f:]+$ ]] || return 1 + + if [[ ${base} == *::* ]]; then + [[ ${base#*::} != *::* ]] || return 1 + left=${base%%::*} + right=${base#*::} + left_count=$(_validate_ipv6_part_count "${left}") || return 1 + right_count=$(_validate_ipv6_part_count "${right}") || return 1 + total_count=$((left_count + right_count)) + (( total_count < 8 )) || return 1 + return 0 + fi + + total_count=$(_validate_ipv6_part_count "${base}") || return 1 + (( total_count == 8 )) } validate_port() { diff --git a/tests/test_common.sh b/tests/test_common.sh index 452cf3a..d225100 100644 --- a/tests/test_common.sh +++ b/tests/test_common.sh @@ -26,7 +26,11 @@ assert_status 1 "$(status_of validate_ipv4 '')" 'validate_ipv4 should reject emp assert_status 0 "$(status_of validate_ipv6 '::1')" 'validate_ipv6 should accept loopback' assert_status 0 "$(status_of validate_ipv6 '2001:db8::1')" 'validate_ipv6 should accept compressed address' assert_status 0 "$(status_of validate_ipv6 'fe80::1%eth0')" 'validate_ipv6 should accept scoped address' +assert_status 0 "$(status_of validate_ipv6 '::ffff:192.0.2.128')" 'validate_ipv6 should accept embedded ipv4 tail' assert_status 1 "$(status_of validate_ipv6 ':::1')" 'validate_ipv6 should reject malformed address' +assert_status 1 "$(status_of validate_ipv6 '2001::1::1')" 'validate_ipv6 should reject multiple compression markers' +assert_status 1 "$(status_of validate_ipv6 '12345::1')" 'validate_ipv6 should reject oversized hextet' +assert_status 1 "$(status_of validate_ipv6 '2001:db8::1::')" 'validate_ipv6 should reject trailing double compression' assert_status 1 "$(status_of validate_ipv6 '1.2.3.4')" 'validate_ipv6 should reject ipv4 literal' assert_status 1 "$(status_of validate_ipv6 '')" 'validate_ipv6 should reject empty string'