Skip to content

PhoneAPI: gate local admin on the connection, not the wire from#11033

Open
caveman99 wants to merge 3 commits into
developfrom
lockdown-admin-gate
Open

PhoneAPI: gate local admin on the connection, not the wire from#11033
caveman99 wants to merge 3 commits into
developfrom
lockdown-admin-gate

Conversation

@caveman99

@caveman99 caveman99 commented Jul 16, 2026

Copy link
Copy Markdown
Member

Build config affected: nRF52 with MESHTASTIC_ENABLE_LOCKDOWN=1 (which co-defines
MESHTASTIC_PHONEAPI_ACCESS_CONTROL and MESHTASTIC_ENCRYPTED_STORAGE).

The per-connection admin check in handleToRadioPacket was guarded by if (p.from == 0 && ...):

if (p.from == 0 && p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) {
    ...
    if (!getAdminAuthorized()) return false;   // only reached when from == 0
}

p.from is a client-supplied wire field. The outer lockdown gate lets a decoded ADMIN_APP
packet addressed to us through without checking from, and MeshService::handleToRadio
rewrites p.from = 0 before AdminModule runs. So a client that sets from != 0 skips the
!getAdminAuthorized() drop entirely, the packet is normalized back to a local identity, and
AdminModule treats it as authorized local admin. An unauthorized local connection can then
run arbitrary admin once the device has been unlocked.

Every packet reaching handleToRadioPacket already originates from the local connection, so
whether it is "local admin" is a property of the connection, not of the wire from. The
decision now lives in classifyLocalAdminPacket(), which ignores from and keys only on the
admin variant and the connection's authorization: lockdown_auth is delivered inline (it is
itself the authentication), any other admin from an unauthorized connection is dropped, and an
authorized connection passes through. Authorized flows are unchanged.

The classifier is a static method compiled unconditionally, so it is unit-testable; the guarded
caller (built only in the lockdown config) calls it. No CI target builds
MESHTASTIC_ENABLE_LOCKDOWN=1, so the guarded caller is verified by inspection while the
security-critical decision is covered by the test.

Test (test_stream_api): an unauthorized connection's ADMIN_APP packet with from != 0 is
classified DropUnauthorized; an authorized connection's identical packet passes through;
lockdown_auth is delivered inline regardless of from; a non-admin packet is outside the gate.

Summary by CodeRabbit

  • Security Improvements
    • Strengthened phone-to-radio administration controls by validating authorization through the active connection rather than relying on packet-provided sender information.
    • Unauthorized administrative commands are now rejected consistently, including messages with spoofed sender details.
    • Lockdown authentication continues to work inline, while authorized administrative traffic passes through normally.
  • Bug Fixes
    • Improved handling of malformed, non-administrative, and unauthorized packets.

The lockdown admin check in handleToRadioPacket only ran when p.from == 0. from is
a client-supplied wire field, and MeshService::handleToRadio rewrites it to 0 before
AdminModule sees the packet. A client could therefore set from != 0 to skip the
!getAdminAuthorized() drop, then have the packet normalized back to a local-admin
identity and executed - unauthorized admin from an unauthorized connection.

Every packet in handleToRadioPacket already comes from the local connection, so
locality is a property of the connection, not of from. Move the decision into
classifyLocalAdminPacket(), which ignores from and keys only on the admin variant and
the connection's authorization: lockdown_auth is delivered inline, any other admin
from an unauthorized connection is dropped, authorized admin passes through.

The classifier is compiled unconditionally and unit-tested; the guarded caller (built
only in the nRF52 lockdown config) calls it. Test: an unauthorized connection's
ADMIN_APP packet with from != 0 is classified DropUnauthorized.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 68d88c0a-a9b5-4ea2-8c59-ba32a91cedd2

📥 Commits

Reviewing files that changed from the base of the PR and between bff0ba8 and 6bcc699.

📒 Files selected for processing (2)
  • src/mesh/PhoneAPI.cpp
  • src/mesh/PhoneAPI.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/mesh/PhoneAPI.cpp

📝 Walkthrough

Walkthrough

PhoneAPI now classifies phone-to-radio admin packets using connection authorization instead of the packet’s wire sender, handles lockdown authentication inline, drops unauthorized admin traffic, and adds tests covering spoofed sender IDs and non-admin packets.

Changes

Local admin authorization

Layer / File(s) Summary
Admin packet classifier
src/mesh/PhoneAPI.h, src/mesh/PhoneAPI.cpp
Adds LocalAdminGate classifications and decodes ADMIN_APP payloads according to connection authorization, with special handling for lockdown_auth.
Authorization gate integration and tests
src/mesh/PhoneAPI.cpp, test/test_stream_api/test_main.cpp
Routes packet handling through the classifier, removes the p.from == 0 requirement, preserves passphrase wiping, and tests unauthorized, authorized, lockdown-auth, and non-admin cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bugfix

Suggested reviewers: thebentern

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: moving local-admin gating from packet from-field to connection-based authorization.
Description check ✅ Passed The description covers the bug, root cause, fix, tests, and deployment scope, though it omits the template's attestation checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lockdown-admin-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/mesh/PhoneAPI.h (1)

332-338: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the docstring to comply with comment length guidelines.

The documentation comment on this function spans 5 lines, which exceeds the repository's guideline of one or two lines maximum. As per coding guidelines, "Keep code comments minimal—one or two lines maximum—and comment only when the reason is not obvious; do not restate straightforward code or add multi-paragraph explanatory blocks."

♻️ Proposed refactor
-    /// Classify a phone->radio packet for the lockdown admin gate. Deliberately independent of the
-    /// wire `from` field: every packet here originates from the local connection and the phone does
-    /// not own `from` (MeshService rewrites it), so keying the gate on `from` lets a client bypass
-    /// it by setting `from != 0`. Static and free of connection state so it is unit-testable; the
-    /// caller supplies the connection's authorization and consumes `outAdmin` for the lockdown case.
+    /// Classify a phone->radio packet for the lockdown admin gate (independent of the wire `from`
+    /// field to prevent bypass).
     static LocalAdminGate classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
                                                    meshtastic_AdminMessage &outAdmin);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/PhoneAPI.h` around lines 332 - 338, Shorten the documentation
comment for classifyLocalAdminPacket to one or two concise lines, retaining only
the non-obvious security rationale that classification must ignore the packet’s
from field and use the connection’s authorization.

Source: Coding guidelines

src/mesh/PhoneAPI.cpp (1)

1737-1739: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the comment to comply with comment length guidelines.

This comment spans 3 lines, which exceeds the repository's guideline of one or two lines maximum. As per coding guidelines, "Keep code comments minimal—one or two lines maximum—and comment only when the reason is not obvious; do not restate straightforward code or add multi-paragraph explanatory blocks."

♻️ Proposed refactor
-    // Gate on the connection, not the wire `from`. `from` is attacker-controlled and MeshService
-    // rewrites it to 0 downstream, so keying this on `from == 0` let a client bypass the check by
-    // sending `from != 0` and have AdminModule treat it as authorized local admin.
+    // Gate on the connection, not the wire `from`, because `from` can be bypassed by an
+    // attacker setting it to a non-zero value prior to normalization.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/PhoneAPI.cpp` around lines 1737 - 1739, Shorten the comment above
the connection gate to no more than two lines while preserving only the
essential rationale: authorization must depend on the connection because the
attacker-controlled from value is rewritten downstream and can bypass the check.

Source: Coding guidelines

test/test_stream_api/test_main.cpp (1)

423-425: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the comment to comply with comment length guidelines.

This comment spans 3 lines, which exceeds the repository's guideline of one or two lines maximum. As per coding guidelines, "Keep comments minimal—one or two lines—and comment only when the reason is not obvious."

♻️ Proposed refactor
-// The lockdown admin gate must decide on the connection's authorization, not the wire `from`. A
-// client that sets from != 0 previously skipped the gate, so an unauthorized connection could run
-// admin. classifyLocalAdminPacket ignores `from`, so the same spoofed packet is still dropped.
+// The lockdown admin gate must decide on the connection's authorization, not the wire `from`.
+// classifyLocalAdminPacket ignores `from`, ensuring a spoofed packet is still dropped.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_stream_api/test_main.cpp` around lines 423 - 425, Shorten the
comment describing the lockdown admin authorization behavior to no more than two
lines, preserving only the essential reason: authorization must use the
connection rather than the spoofable wire from value.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mesh/PhoneAPI.cpp`:
- Around line 1745-1750: Update the cleanup block around
handleLockdownAuthInline to explicitly wipe the encoded passphrase stored in
p.decoded.payload.bytes before returning true. Keep the existing
admin.lockdown_auth.passphrase.bytes cleanup, and ensure the MeshPacket payload
bytes are cleared using the same secure volatile wipe approach.

---

Nitpick comments:
In `@src/mesh/PhoneAPI.cpp`:
- Around line 1737-1739: Shorten the comment above the connection gate to no
more than two lines while preserving only the essential rationale: authorization
must depend on the connection because the attacker-controlled from value is
rewritten downstream and can bypass the check.

In `@src/mesh/PhoneAPI.h`:
- Around line 332-338: Shorten the documentation comment for
classifyLocalAdminPacket to one or two concise lines, retaining only the
non-obvious security rationale that classification must ignore the packet’s from
field and use the connection’s authorization.

In `@test/test_stream_api/test_main.cpp`:
- Around line 423-425: Shorten the comment describing the lockdown admin
authorization behavior to no more than two lines, preserving only the essential
reason: authorization must use the connection rather than the spoofable wire
from value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eb6c8b76-4c41-4aba-832e-62863050598b

📥 Commits

Reviewing files that changed from the base of the PR and between f18a8b0 and bff0ba8.

📒 Files selected for processing (3)
  • src/mesh/PhoneAPI.cpp
  • src/mesh/PhoneAPI.h
  • test/test_stream_api/test_main.cpp

Comment thread src/mesh/PhoneAPI.cpp Outdated
@caveman99

Copy link
Copy Markdown
Member Author

All three addressed in 6bcc699.

Encoded passphrase wipe (Major): correct - handleLockdownAuthInline takes admin.lockdown_auth by reference and only zeroes that decoded copy; the still-encoded passphrase in p.decoded.payload.bytes was left in the packet buffer, and the comment I carried forward wrongly claimed otherwise. Now wiping both the decoded scratch and the encoded packet bytes, and fixed the comment.

Two comment-length nitpicks: shortened the classifyLocalAdminPacket docstring and the gate comment to two lines each, keeping the non-obvious rationale (ignore the wire from).

Note this whole block is in the MESHTASTIC_ENCRYPTED_STORAGE && MESHTASTIC_PHONEAPI_ACCESS_CONTROL guard, which no CI target compiles, so these changes are verified by inspection; the from-independent decision they wrap remains covered by the classifyLocalAdminPacket unit test.

@caveman99
caveman99 requested a review from thebentern July 16, 2026 20:53
@github-actions

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

@thebentern thebentern added the bugfix Pull request that fixes bugs label Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants