Skip to content

7 Novel Vulnerabilities in shamir.py — Static Analysis Report #79

@antvpk

Description

@antvpk

7 Novel Vulnerabilities in shamir.py — Static Analysis Report

Hello @bitaps-com team,

I have conducted a thorough static and runtime analysis of the shamir_secret_sharing.py implementation in the PyBTC library as part of the ZeroNights X bug bounty program.

Bug 1 — assert in _gf256_div silently removed in production (python -O)

Severity: High
Location: _gf256_div(), line 55

The line assert a == _gf256_mul(r, b) is the only verification that GF(256) division produced a correct result. Python removes all assert statements when executed with the -O (optimize) flag, which is standard practice in many production deployments. Any arithmetic error therefore silently propagates incorrect GF elements into every share for the affected byte, with no exception raised.

# Current — silently gone with python -O
assert a == _gf256_mul(r, b)

# Fix
if _gf256_mul(r, b) != a:
    raise ArithmeticError("GF256 division verification failed")

Bug 2 — restore_secret({}) crashes with TypeError instead of a clean error

Severity: High
Location: restore_secret(), end of validation block

Passing an empty dict leaves share_length = None. The subsequent for i in range(share_length) raises TypeError: 'NoneType' object cannot be interpreted as an integer — an internal crash rather than a meaningful error message. Confirmed by execution.

# Fix — add at entry
if not shares:
    raise ValueError("No shares provided")

Bug 3 — Empty secret b"" splits silently but restore_secret always fails

Severity: Medium
Location: split_secret() line 114 vs restore_secret() line 141

split_secret(2, 3, b"") succeeds without error (the byte loop runs 0 iterations), producing shares of length 0. restore_secret then raises "Invalid shares" because share_length == 0 is explicitly rejected. The API is asymmetric: a user who accidentally splits an empty secret believes they hold a valid backup when recovery is structurally impossible.

# Fix in split_secret
if len(secret) == 0:
    raise ValueError("Secret must not be empty")

Bug 4 — Duplicate share indices cause unhandled ZeroDivisionError inside interpolation

Severity: Medium
Location: restore_secret()_interpolation()

restore_secret never validates that all share indices are distinct. If two shares carry the same index, the Lagrange denominator GF_sub(x_j, x_m) = x_j XOR x_m = 0, which triggers ZeroDivisionError deep inside _gf256_div with no descriptive context. A caller assembling shares from user input can trivially produce this condition.

# Fix
if len(set(shares.keys())) != len(shares):
    raise ValueError("Share indices must be unique")

Bug 5 — _gf256_pow is O(b) loop; should be O(1) via precomputed tables

Severity: Medium / DoS
Location: _gf256_pow() line 28, called from _fn()

_gf256_pow(a, b) multiplies in a loop b-1 times. For threshold=200 and a 32-byte secret this causes approximately 636,800 unnecessary multiplications that the already-present exp/log tables make entirely redundant. This is a DoS surface on resource-constrained devices and is inconsistent with every other arithmetic primitive in the file.

# Fix — O(1)
def _gf256_pow(a, b):
    if b == 0: return 1
    if a == 0: return 0
    return EXP_TABLE[(LOG_TABLE[a] * b) % 255]

Bug 6 — restore_secret(x=0) parameter is publicly exposed — silent wrong output

Severity: Low
Location: restore_secret() function signature

x=0 is the Lagrange evaluation target and must always be 0 to recover f(0) = secret. Exposing it as a public keyword argument allows a caller to accidentally pass x=1 and silently receive a share value instead of the secret, with no error raised.

# Fix — remove the parameter
def restore_secret(shares):   # x=0 is an internal detail

Bug 7 — import time is a dead import

Severity: Info
Location: Line 2

time is imported but referenced nowhere in the file. In a cryptographic library this is worth flagging: it suggests timing-based code (possibly entropy seeding) was removed incompletely, and it is worth confirming no timing-dependent security property was inadvertently removed with it.


Verification

All bugs above were identified through static code analysis and confirmed via local execution. No live shares, mnemonics, or wallet keys were used in this research.


Bounty

If any of these findings qualify for a bounty reward, please send to:

Bitcoin address: bc1qw40vafh6cl57hxu8696arnsnqcmvv06qt9hhj2

Thank you

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions