Skip to content

refactor(security): harden mac_address_spoof_automator input validation and failure handling - #2

Merged
krishsharma-code merged 2 commits into
mainfrom
audit/review-mac-address-spoof-automator
Oct 25, 2026
Merged

krishsharma-code merged 2 commits into
mainfrom
audit/review-mac-address-spoof-automator

Conversation

@krishsharma-code

Copy link
Copy Markdown
Owner

Summary

Static code-quality and security review of python_scripts/mac_address_spoof_automator.py (Day 12 - System and Network Enumeration). The script brings an interface down, assigns a new MAC address and brings the interface back up. The review found one genuine availability defect, an unanchored input-validation regex, a missing privilege pre-check, and a MAC generator that emitted a real vendor OUI. This PR fixes those, adds a --dry-run mode, and adds a focused test module.

Linked Issue

No tracking issue - this is a routine code-quality audit of an existing script.

Type of Change

  • New script
  • Bug fix
  • Documentation update
  • Refactor (no behaviour change)
  • Chore / tooling / CI

What Changed

  • Failure handling (defect). change_mac() ran down then address then up. If the address step failed (bad MAC, dropped privileges), the interface was left down - a network outage on the host. The up step now runs in a finally block, so the interface is always restored, and a failure to restore is reported rather than swallowed.
  • Input validation (defect). The MAC check used an unanchored re.match(r"[0-9a-f]{2}([:][0-9a-f]{2}){5}", ...), which accepted trailing junk such as aa:bb:cc:dd:ee:ff:zz or aa:bb:cc:dd:ee:ff00. Replaced with a compiled, end-anchored _MAC_RE used via fullmatch().
  • Interface validation (defect). The interface argument was passed straight through. A value such as -x would be read as an option by ip. Added _INTERFACE_RE (first character alphanumeric, IFNAMSIZ-1 length cap) and an is_valid_interface() guard.
  • Privilege handling. Added an early os.geteuid() != 0 check and an ip-on-PATH check, so the script fails cleanly before touching the interface instead of part-way through.
  • MAC generation. get_random_mac() used the fixed Xen OUI 00:16:3e, so spoofed hosts impersonated a real vendor prefix and could collide. It now sets the locally-administered bit (0x02), clears the multicast bit, and randomises the whole address.
  • CLI. Replaced the raw sys.argv parsing with argparse (per CONTRIBUTING.md), which supplies a --help path and a new --dry-run flag that prints the exact commands without changing anything.
  • Docs. Module docstring now documents behaviour, safety and exit codes; every function has an Args/Returns docstring; the non-obvious bit-twiddling and rollback logic are commented.
  • Tests. Added tests/test_mac_address_spoof_automator.py (24 cases) covering detection-adjacent validation, redaction-equivalent safety (interface rollback), generation invariants and the exit-code contract.

New or Modified Scripts

Script Language Change
python_scripts/mac_address_spoof_automator.py Python Validate MAC + interface input, restore interface on failure, add root pre-check and --dry-run, argparse CLI, docstrings
tests/test_mac_address_spoof_automator.py Python New test module (24 cases)

Safety Review

  • This script only performs actions the user explicitly requests
  • Any destructive operation requires an explicit flag and supports --dry-run
  • Any state-changing action is printed before it is carried out
  • No credentials, API keys or tokens are committed
  • No hardcoded personal or machine-specific paths

Testing Checklist

  • python3 -m py_compile python_scripts/*.py passes
  • flake8 python_scripts --select=E9,F63,F7,F82,F821,F823 --max-line-length=120 passes
  • for f in bash_scripts/*.sh; do bash -n "$f"; done passes (untouched)
  • shellcheck bash_scripts/*.sh reports no errors (advisory, untouched)
  • I ran the new or changed script end to end on a machine I am permitted to test
  • I tested the --help output
  • I tested the failure path (missing argument, bad path, no permissions)
  • No new console warnings or unhandled errors

Local run: python3 -m pytest tests/ -q -> 50 passed (24 new + 26 existing).

Documentation

  • The script is already listed in the index in README.md (Day 12, Network Enumeration)
  • The script has a module docstring explaining what it does
  • Non-obvious logic is commented

Code Quality Checklist

  • My code follows the standards in CONTRIBUTING.md
  • Imports are at the top of the file, not inside functions
  • I have performed a self-review of my own code
  • I have removed debugging code and unused imports

Additional Notes

Behaviour is preserved except where a genuine defect is fixed (rollback on failure, stricter validation, --dry-run, argparse). The MAC generator change means newly generated addresses are locally administered rather than impersonating the Xen OUI; this is a correctness fix for a spoofing tool, not a stylistic preference. random is retained over secrets because a MAC address is broadcast on the wire and carries no secrecy requirement.

@krishsharma-code krishsharma-code left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review of PR #2 — mac_address_spoof_automator.py.

Submitted as COMMENT because GitHub does not allow approving your own pull request. Scope: one script and its new test module.

What was reviewed

Security boundaries (input validation, privilege handling), failure/edge-case handling, memory footprint, and docstring/comment clarity for python_scripts/mac_address_spoof_automator.py.

What was found

  1. Availability defect (high). change_mac() brought the interface down, then set the address, then brought it up — with no rollback. Any failure on the middle step left the interface down. A tool whose whole job is to stay unnoticed should not knock the host off the network when it fails.
  2. Input validation (medium). The MAC regex was applied with re.match() and no end anchor, so aa:bb:cc:dd:ee:ff:zz passed validation and was handed to ip. The interface argument was not validated at all, so -x would be parsed as an option.
  3. Privilege handling (medium). No pre-flight root check; the script failed mid-sequence rather than before touching the interface.
  4. Correctness (low/medium). get_random_mac() used the fixed Xen OUI 00:16:3e and never set the locally-administered bit — a spoofed MAC that impersonates a real vendor prefix and can collide.
  5. Standards (low). Raw sys.argv parsing instead of argparse (CONTRIBUTING.md), thin docstrings, and no test module.

What changed and why

See the inline comments. In short: up now runs in a finally; both inputs are validated with anchored, compiled patterns; a root/PATH pre-check runs before any state change; the generator now emits locally-administered unicast addresses; the CLI moved to argparse with a --dry-run; and 24 tests cover the validation, rollback and exit-code contract. Memory footprint is unchanged and O(1) — the script never buffers file contents.

Verification: python3 -m py_compile python_scripts/*.py and flake8 --select=E9,F63,F7,F82,F821,F823 --max-line-length=120 pass; python3 -m pytest tests/ -q reports 50 passed (24 new).

except subprocess.CalledProcessError as exc:
print(f"[-] Error: Failed to change MAC address. "
f"Ensure you have root privileges: {exc}")
finally:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key fix. Bringing the interface back up now happens in a finally, so a failure in the address step (bad MAC, lost privileges, driver rejection) can no longer leave the host offline. The inner try/except deliberately catches CalledProcessError only — if up itself fails we surface a warning rather than masking the original error. Is catching only CalledProcessError the right boundary here, or should a missing ip binary (FileNotFoundError) also be handled at this level?

# A MAC address is six colon-separated hex octets. The trailing \Z anchors the
# match so junk such as "aa:bb:cc:dd:ee:ff:zz" or "aa:bb:cc:dd:ee:ff00" is
# rejected rather than silently accepted by an unanchored re.match().
_MAC_RE = re.compile(r"[0-9a-f]{2}(?::[0-9a-f]{2}){5}\Z")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End-anchored with \Z and used via fullmatch(), so trailing junk (aa:bb:cc:dd:ee:ff:zz, aa:bb:cc:dd:ee:ff00) is now rejected. Previously re.match() accepted it and passed the value straight to ip link set.

# consist of letters, digits and a few punctuation characters. The first
# character must be alphanumeric so a fat-fingered or hostile argument such as
# "-x" cannot be interpreted as an option by `ip`.
_INTERFACE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,14}\Z")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New interface-name guard. Requiring an alphanumeric first character stops a value like -x being interpreted as an option by ip; the 15-character cap matches IFNAMSIZ-1. Should we also verify the interface actually exists (e.g. against /sys/class/net) before attempting the change, or is that better left to ip itself?

return 1

if not args.dry_run:
if os.geteuid() != 0:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root and ip-on-PATH checks now run before any state change, so a non-root invocation fails cleanly instead of half-way through. --dry-run intentionally bypasses these so the commands can be inspected without privileges.

random.randint(0x00, 0xff),
random.randint(0x00, 0xff)]
return ':'.join(map(lambda x: "%02x" % x, mac))
first_octet = (random.randint(0x00, 0xFF) | _LOCALLY_ADMINISTERED) & ~_MULTICAST

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generator correctness fix: the locally-administered bit is set and the multicast bit cleared, and the whole address is randomised. The old fixed OUI 00:16:3e (Xen) made spoofed hosts impersonate a real vendor prefix. random is kept over secrets on purpose — a MAC address is broadcast on the wire, so unpredictability is not a security requirement here.

True when the string is exactly six colon-separated hex octets.
"""
Configures the network interface with a new MAC address.
return bool(_MAC_RE.fullmatch(value.lower()))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fullmatch() + \Z makes the anchoring explicit and readable; the previous inline re.match(...) call both failed to anchor and recompiled the pattern on every invocation.

@krishsharma-code
krishsharma-code merged commit dd44701 into main Oct 25, 2026
2 checks passed
@krishsharma-code
krishsharma-code deleted the audit/review-mac-address-spoof-automator branch October 25, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant