lightning-onion icon indicating copy to clipboard operation
lightning-onion copied to clipboard

Fix hop payload bounds

Open erickcestari opened this issue 3 weeks ago • 2 comments

unwrapPacket passes the full 2600-byte hopInfo buffer to DecodeHopPayload, but only the first 1300 bytes contain routing info. The remaining 1300 bytes are zero-padding used for XOR decryption.

A malformed packet with a large payload size field can cause the decoder to read beyond the routing info into the padding area. This creates parsing inconsistencies with other implementations that enforce the 1300-byte boundary.

Found through differential fuzzing (bitcoinfuzz) where Core Lightning and rust-lightning rejected the malformed onion packet.

erickcestari avatar Dec 11 '25 11:12 erickcestari

Summary of Changes

Hello @erickcestari, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a critical fix to how onion packet payloads are processed, specifically preventing DecodeHopPayload from reading beyond the intended routing information boundary. Previously, a malformed packet could exploit this by specifying an oversized payload, causing the decoder to read into the zero-padding area. The change ensures that only the valid routing information portion of the hopInfo buffer is processed, enhancing parsing consistency and robustness against malformed inputs. Additionally, new tests have been added to validate this boundary enforcement, alongside minor test function name corrections.

Highlights

  • Payload Boundary Enforcement: The unwrapPacket function now explicitly limits the hopInfo buffer passed to DecodeHopPayload to routingInfoLen (1300 bytes). This prevents the decoder from reading into the zero-padding area of the buffer, addressing a potential parsing inconsistency with malformed packets.
  • New Test Case for Malformed Packets: A new test, TestUnwrapPacketBeyondRoutingInfoBoundary, has been added to verify that unwrapPacket correctly handles malformed onion packets where the payload size attempts to exceed the routing information boundary, ensuring it fails as expected.
  • Test Function Renames: Several test functions in sphinx_test.go were renamed to correct a typo, changing 'Relpay' to 'Replay' (e.g., TestSphinxNodeRelpay is now TestSphinxNodeReplay).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

[^1]: Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot] avatar Dec 11 '25 11:12 gemini-code-assist[bot]

Maybe we should also change DecodeHopPayload to enforce the correct size, rather than UINT16_MAX.

morehouse avatar Dec 11 '25 15:12 morehouse

Maybe we should also change DecodeHopPayload to enforce the correct size, rather than UINT16_MAX.

Updated! DecodeHopPayload now validates that the payload size does not exceed MaxHopPayloadSize (1265 bytes) rather than allowing sizes up to UINT16_MAX.

I have created a new constant MaxHopPayloadSize which is defined as MaxRoutingPayloadSize - BigSizeLenMaxBytes - HMACSize, which accounts for the 3-byte BigSize length prefix and 32-byte HMAC within the 1300-byte routing info boundary. If the encoded payload size exceeds this limit, DecodeHopPayload returns ErrMaxHopPayloadSizeExceeded.

erickcestari avatar Dec 15 '25 18:12 erickcestari

A malformed packet with a large payload size field can cause the decoder to read beyond the routing info into the padding area. This creates parsing inconsistencies with other implementations that enforce the 1300-byte boundary.

IIUC it still results in the malformed packet being rejected? Or which inconsistencies are you referring to here?

Roasbeef avatar Dec 18 '25 00:12 Roasbeef

IIUC it still results in the malformed packet being rejected? Or which inconsistencies are you referring to here?

As you said, the packet would be forwarded to the next hop, where it would be rejected as invalid due to its invalid HMAC. This assumes that the packet has the required TLVs to be considered a forwarded packet. If it's a final-hop payload, it would fail locally because of the non-zero HMAC.

erickcestari avatar Dec 18 '25 13:12 erickcestari

The fix doesn't require peeking or changing DecodeHopPayload. It simply limits the input reader:

Yeah, I understood that. My comment was on @Roasbeef earlier comment. Sorry for the confusion.

gijswijs avatar Dec 18 '25 14:12 gijswijs

@erickcestari So with the current solution, a malformed packet could still look into the bytes of the 32-byte HMAC, correct?

gijswijs avatar Dec 18 '25 14:12 gijswijs

The fix doesn't require peeking or changing DecodeHopPayload. It simply limits the input reader:

Yeah, I understood that. My comment was on @Roasbeef earlier comment. Sorry for the confusion.

Yes, I only saw it after I commented 😅.

@erickcestari So with the current solution, a malformed packet could still look into the bytes of the 32-byte HMAC, correct?

No. If a malformed payload tries to read into the HMAC area, the subsequent 32-byte HMAC read will fail with io.ErrUnexpectedEOF due to insufficient remaining bytes.

For example, with a 1300-byte boundary: if a payload claims 1280 bytes, after reading the 3-byte length prefix + 1280-byte payload, only 17 bytes remain. Not enough for the 32-byte HMAC read.

Am I missing something?

erickcestari avatar Dec 18 '25 15:12 erickcestari