(Specifications) A bug in specifications with no direct impact on client implementations
Description
Brief/Intro
When asking for an Ethereum Node Record (ENR) of another node, this one should respond with a packet containing this information.
It is specified that ENR should be long of at most 300 bytes but this check is missing when decoding these packets.
Vulnerability Details
In the EIP-778 defining ENR, it is stated that "The maximum encoded size of a node record is 300 bytes. Implementations should reject records larger than this size."
When receiving an ENR_RESPONSE packet, the packet is first deserialized with the ENRResponsePacketData::readFrom function.
This function RLP decodes the packet data from raw RLP by using the discovery library by calling its function fromBytes but this library contains a bug.
Indeed, it doesn't check if the RLP-encoded packet data size exceeds 300 bytes.Here is the definition of the fromBytes function which seems to check for the size of some data not exceeding 300 bytes, but this is only checking that the value of the id in the key-value store of the ENR does not exceed 300 bytes. This seems to be a misunderstanding with the specification, that says that the RLP-encoded record should not exceed 300 bytes.
To conclude, a peer can send an unexpectedly big ENR to our node when responding with the ENRResponse packet and it's going to be accepted and stored.
Impact Details
This is a drift from the specification and will make the implementation waste resources by allowing huge UDP packets of the ENRResponse as well as storing abnormally big records on disk.
References
Links added where applicable
Proof of Concept
Allow serialization or the ENR without limit check in the call to Packet.create in order to build a simple POC
diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java
index d71c7e1e0..642dc603c 100644
--- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java
+++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java
@@ -55,7 +55,8 @@ public class ENRResponsePacketData implements PacketData {
public void writeTo(final RLPOutput out) {
out.startList();
out.writeBytes(requestHash);
- out.writeRLPBytes(enr.serialize());
+// out.writeRLPBytes(enr.serialize());
+ out.writeRLPBytes(enr.asRlp());
out.endList();
}
If you're worried about the patch to the writeTo making the POC possible, revert this patch and instead build the packet from the RLP data, which doesn't check the size of the ENR record as expected.
diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java
index e7e113bfb..6d39702ab 100644
--- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java
+++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java
@@ -54,12 +54,11 @@ public class Packet {
private final SECPSignature signature;
private final SECPPublicKey publicKey;
- private Packet(final PacketType type, final PacketData data, final NodeKey nodeKey) {
+ private Packet(final PacketType type, final PacketData data, final NodeKey nodeKey, final Bytes dataBytes) {
this.type = type;
this.data = data;
final Bytes typeBytes = Bytes.of(this.type.getValue());
- final Bytes dataBytes = RLP.encode(this.data::writeTo);
this.signature = nodeKey.sign(keccak256(Bytes.wrap(typeBytes, dataBytes)));
this.hash = keccak256(Bytes.concatenate(encodeSignature(signature), typeBytes, dataBytes));
@@ -96,7 +95,12 @@ public class Packet {
public static Packet create(
final PacketType packetType, final PacketData packetData, final NodeKey nodeKey) {
- return new Packet(packetType, packetData, nodeKey);
+ return new Packet(packetType, packetData, nodeKey, RLP.encode(packetData::writeTo));
+ }
+
+ public static Packet create2(
+ final PacketType packetType, final PacketData packetData, final NodeKey nodeKey, final Bytes dataBytes) {
+ return new Packet(packetType, packetData, nodeKey, dataBytes);
}
public static Packet decode(final Buffer message) {