From 9a36b1a5fa2a0a56d2afd18d3fdd6dc03650859d Mon Sep 17 00:00:00 2001 From: "ashkan@beigi.net" Date: Thu, 2 Jul 2026 02:56:20 +0000 Subject: [PATCH] SAIP: make _split_der robust to truncated/malformed input decode_saip_profile (used by make_profile --inspect on user files) could IndexError on a truncated tail. _split_der now stops at the last complete DER element and returns the valid prefix. Tests: +1; 109 pass. Co-Authored-By: Claude Opus 4.8 --- server/saip_profile.py | 14 ++++++++++++-- tests/test_saip.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/server/saip_profile.py b/server/saip_profile.py index 8c3aa87..465aa71 100644 --- a/server/saip_profile.py +++ b/server/saip_profile.py @@ -142,7 +142,11 @@ def create_saip_profile( def _split_der(data: bytes): - """Yield byte-range slices of consecutive top-level DER TLVs in ``data``.""" + """Yield byte-range slices of consecutive top-level DER TLVs in ``data``. + + Stops cleanly at the last complete element so a truncated or malformed tail + yields the valid prefix instead of raising. + """ i, n = 0, len(data) while i < n: start = i @@ -151,15 +155,21 @@ def _split_der(data: bytes): if first & 0x1F == 0x1F: # high-tag-number form: skip continuation octets while i < n and data[i] & 0x80: i += 1 - i += 1 + i += 1 # final tag byte + if i >= n: # no room for a length octet + return length_byte = data[i] i += 1 if length_byte & 0x80: # long-form length num = length_byte & 0x7F + if i + num > n: + return length = int.from_bytes(data[i:i + num], "big") i += num else: length = length_byte + if i + length > n: # truncated content + return i += length yield data[start:i] diff --git a/tests/test_saip.py b/tests/test_saip.py index 95badd1..fb32ed0 100644 --- a/tests/test_saip.py +++ b/tests/test_saip.py @@ -80,6 +80,17 @@ class TestSaipEncoding: assert by["header"]["major-version"] == 3 assert by["mf"]["ef-iccid"] == [("fillFileContent", enc_iccid(iccid))] + @saip_required + def test_truncated_profile_yields_prefix_not_crash(self): + pkg = saip.create_saip_profile("302720000000001", generate_ki(), + b"\x22" * 16, generate_iccid()) + full = saip.decode_saip_profile(pkg) + # Cutting mid-element must not raise; it returns the complete prefix. + partial = saip.decode_saip_profile(pkg[:-3]) + assert len(partial) < len(full) + assert [c for c, _ in partial] == [c for c, _ in full][:len(partial)] + assert saip.decode_saip_profile(b"\xa0\x82") == [] # bogus length, no crash + @saip_required def test_imsi_embedded_in_usim(self): imsi = "302720123456789"