Skip to content

Commit 2a7d885

Browse files
committed
feat: Add more protection
1 parent 85447b9 commit 2a7d885

File tree

6 files changed

+499
-67
lines changed

6 files changed

+499
-67
lines changed

packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go

Lines changed: 275 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,7 +1380,7 @@ func TestEIP1559DynamicFeeTransaction(t *testing.T) {
13801380

13811381
// Common setup for both test approaches
13821382
maxFeePerGas := new(big.Int).Mul(env.MustGetGasPrice(), big.NewInt(2)) // 2x base fee
1383-
maxPriorityFeePerGas := big.NewInt(1000000000) // 1 Gwei tip
1383+
maxPriorityFeePerGas := big.NewInt(10000000000) // 1 Gwei tip
13841384

13851385
t.Run("native geth type", func(t *testing.T) {
13861386
// Test using eth_sendRawTransaction with manually constructed DynamicFeeTx
@@ -1458,7 +1458,7 @@ func TestEIP1559DynamicFeeTransactionWithAccessList(t *testing.T) {
14581458
require.NoError(t, err)
14591459

14601460
maxFeePerGas := new(big.Int).Mul(env.MustGetGasPrice(), big.NewInt(2))
1461-
maxPriorityFeePerGas := big.NewInt(1000000000)
1461+
maxPriorityFeePerGas := big.NewInt(10000000000)
14621462
accessList := types.AccessList{{Address: contractAddr, StorageKeys: []common.Hash{common.HexToHash("0x0")}}}
14631463

14641464
t.Run("native geth type", func(t *testing.T) {
@@ -1550,7 +1550,7 @@ func TestEIP4844BlobTransaction(t *testing.T) {
15501550
blobHashes := sidecar.BlobHashes()
15511551

15521552
maxFeePerGas := new(big.Int).Mul(env.MustGetGasPrice(), big.NewInt(2))
1553-
maxPriorityFeePerGas := big.NewInt(1000000000)
1553+
maxPriorityFeePerGas := big.NewInt(10000000000)
15541554
blobFeeCap := maxFeePerGas // Same as maxFeePerGas for simplicity
15551555

15561556
t.Run("native geth type", func(t *testing.T) {
@@ -1565,7 +1565,7 @@ func TestEIP4844BlobTransaction(t *testing.T) {
15651565
Value: uint256.NewInt(2000),
15661566
BlobFeeCap: uint256.MustFromBig(maxFeePerGas),
15671567
BlobHashes: blobHashes,
1568-
Sidecar: sidecar,
1568+
// Sidecar: sidecar, // includes sidecar will make the blob tx exceeds tx size limit
15691569
}
15701570

15711571
signedBlobTx, err := types.SignTx(types.NewTx(blobTx), env.Signer(), from)
@@ -1669,10 +1669,280 @@ func TestInvalidGasPriceConfiguration(t *testing.T) {
16691669
GasPrice: (*hexutil.Big)(env.MustGetGasPrice()),
16701670
Value: (*hexutil.Big)(big.NewInt(1000)),
16711671
MaxFeePerGas: (*hexutil.Big)(env.MustGetGasPrice()),
1672-
MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(1000000000)),
1672+
MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(10000000000)),
16731673
}
16741674

16751675
_, err := env.SendTransaction(args)
16761676
require.Error(t, err)
16771677
require.Contains(t, err.Error(), "both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
16781678
}
1679+
1680+
// TestSendRawTransactionValidation tests the security validations in SendRawTransaction
1681+
func TestSendRawTransactionValidation(t *testing.T) {
1682+
env := newSoloTestEnv(t)
1683+
1684+
t.Run("empty transaction data", func(t *testing.T) {
1685+
var txHash common.Hash
1686+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes{})
1687+
require.Error(t, err)
1688+
require.Contains(t, err.Error(), "empty transaction data")
1689+
})
1690+
1691+
t.Run("oversized transaction data", func(t *testing.T) {
1692+
// Create a transaction larger than 128KB
1693+
oversizedData := make([]byte, 130*1024)
1694+
var txHash common.Hash
1695+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(oversizedData))
1696+
require.Error(t, err)
1697+
require.Contains(t, err.Error(), "transaction size")
1698+
require.Contains(t, err.Error(), "exceeds maximum")
1699+
})
1700+
1701+
t.Run("invalid transaction encoding", func(t *testing.T) {
1702+
// Invalid RLP/binary data
1703+
invalidData := []byte{0xff, 0xff, 0xff}
1704+
var txHash common.Hash
1705+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(invalidData))
1706+
require.Error(t, err)
1707+
require.Contains(t, err.Error(), "invalid transaction encoding")
1708+
})
1709+
1710+
t.Run("dynamic fee transaction validation", func(t *testing.T) {
1711+
from, fromAddr := env.NewAccountWithL2Funds()
1712+
env.accountManager.Add(from)
1713+
_, toAddr := env.NewAccountWithL2Funds()
1714+
1715+
// Create a dynamic fee transaction with zero gas limit (invalid)
1716+
dynamicTx := &types.DynamicFeeTx{
1717+
ChainID: big.NewInt(int64(env.ChainID)),
1718+
Nonce: env.NonceAt(fromAddr),
1719+
GasFeeCap: big.NewInt(10000000000),
1720+
GasTipCap: big.NewInt(500000000),
1721+
Gas: 0, // Invalid: zero gas limit
1722+
To: &toAddr,
1723+
Value: big.NewInt(1000),
1724+
}
1725+
1726+
signedTx, err := types.SignTx(types.NewTx(dynamicTx), env.Signer(), from)
1727+
require.NoError(t, err)
1728+
1729+
rawBytes, err := signedTx.MarshalBinary()
1730+
require.NoError(t, err)
1731+
1732+
var txHash common.Hash
1733+
err = env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(rawBytes))
1734+
require.Error(t, err)
1735+
require.Contains(t, err.Error(), "transaction gas limit cannot be zero")
1736+
})
1737+
1738+
t.Run("blob transaction validation", func(t *testing.T) {
1739+
from, fromAddr := env.NewAccountWithL2Funds()
1740+
env.accountManager.Add(from)
1741+
1742+
// Create a blob transaction with no blobs (invalid)
1743+
_, toAddr := env.NewAccountWithL2Funds()
1744+
blobTx := &types.BlobTx{
1745+
ChainID: uint256.NewInt(uint64(env.ChainID)),
1746+
Nonce: env.NonceAt(fromAddr),
1747+
GasFeeCap: uint256.NewInt(2000000000),
1748+
GasTipCap: uint256.NewInt(10000000000),
1749+
Gas: 21000,
1750+
To: toAddr,
1751+
Value: uint256.NewInt(1000),
1752+
BlobFeeCap: uint256.NewInt(10000000000),
1753+
BlobHashes: []common.Hash{}, // Invalid: blob tx must contain at least one blob
1754+
}
1755+
1756+
signedTx, err := types.SignTx(types.NewTx(blobTx), env.Signer(), from)
1757+
require.NoError(t, err)
1758+
1759+
rawBytes, err := signedTx.MarshalBinary()
1760+
require.NoError(t, err)
1761+
1762+
var txHash common.Hash
1763+
err = env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(rawBytes))
1764+
require.Error(t, err)
1765+
require.Contains(t, err.Error(), "blob transaction must contain at least one blob")
1766+
})
1767+
}
1768+
1769+
// TestSendRawTransactionSecurityHardening tests advanced security validations
1770+
func TestSendRawTransactionSecurityHardening(t *testing.T) {
1771+
env := newSoloTestEnv(t)
1772+
1773+
t.Run("malformed RLP structure", func(t *testing.T) {
1774+
// Create deeply nested RLP structure to test complexity validation
1775+
malformedRLP := []byte{0xc0} // Empty list
1776+
for i := 0; i < 20; i++ { // Create deep nesting
1777+
malformedRLP = append([]byte{0xc1}, malformedRLP...)
1778+
}
1779+
1780+
var txHash common.Hash
1781+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(malformedRLP))
1782+
require.Error(t, err)
1783+
require.Contains(t, err.Error(), "RLP structure too deep")
1784+
})
1785+
1786+
t.Run("invalid transaction type", func(t *testing.T) {
1787+
// Test unsupported transaction type
1788+
invalidTypeData := []byte{0x7f} // Unsupported type 0x7f
1789+
invalidTypeData = append(invalidTypeData, make([]byte, 100)...) // Add some payload
1790+
1791+
var txHash common.Hash
1792+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(invalidTypeData))
1793+
require.Error(t, err)
1794+
require.Contains(t, err.Error(), "unsupported transaction type: 0x7f")
1795+
})
1796+
1797+
t.Run("malicious legacy type as typed", func(t *testing.T) {
1798+
// Test invalid typed transaction with legacy type 0x00
1799+
maliciousData := []byte{0x00, 0xc0} // Type 0x00 with empty payload
1800+
1801+
var txHash common.Hash
1802+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(maliciousData))
1803+
require.Error(t, err)
1804+
require.Contains(t, err.Error(), "invalid typed transaction: legacy type 0x00")
1805+
})
1806+
1807+
t.Run("extremely large gas limit", func(t *testing.T) {
1808+
from, fromAddr := env.NewAccountWithL2Funds()
1809+
env.accountManager.Add(from)
1810+
_, toAddr := env.NewAccountWithL2Funds()
1811+
1812+
// Create transaction with extremely large gas limit
1813+
maliciousTx := &types.LegacyTx{
1814+
Nonce: env.NonceAt(fromAddr),
1815+
GasPrice: env.MustGetGasPrice(),
1816+
Gas: 40000000, // 40M gas - exceeds our 30M limit
1817+
To: &toAddr,
1818+
Value: big.NewInt(1000),
1819+
}
1820+
1821+
signedTx, err := types.SignTx(types.NewTx(maliciousTx), env.Signer(), from)
1822+
require.NoError(t, err)
1823+
1824+
rawBytes, err := signedTx.MarshalBinary()
1825+
require.NoError(t, err)
1826+
1827+
var txHash common.Hash
1828+
err = env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(rawBytes))
1829+
require.Error(t, err)
1830+
require.Contains(t, err.Error(), "transaction gas limit")
1831+
require.Contains(t, err.Error(), "exceeds maximum")
1832+
})
1833+
1834+
t.Run("extremely large data payload", func(t *testing.T) {
1835+
from, fromAddr := env.NewAccountWithL2Funds()
1836+
env.accountManager.Add(from)
1837+
_, toAddr := env.NewAccountWithL2Funds()
1838+
1839+
// Create transaction with extremely large data payload
1840+
largeData := make([]byte, 70*1024) // 70KB data - exceeds limit
1841+
maliciousTx := &types.LegacyTx{
1842+
Nonce: env.NonceAt(fromAddr),
1843+
GasPrice: env.MustGetGasPrice(),
1844+
Gas: 21000,
1845+
To: &toAddr,
1846+
Value: big.NewInt(1000),
1847+
Data: largeData,
1848+
}
1849+
1850+
signedTx, err := types.SignTx(types.NewTx(maliciousTx), env.Signer(), from)
1851+
require.NoError(t, err)
1852+
1853+
rawBytes, err := signedTx.MarshalBinary()
1854+
require.NoError(t, err)
1855+
1856+
var txHash common.Hash
1857+
err = env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(rawBytes))
1858+
require.Error(t, err)
1859+
require.Contains(t, err.Error(), "transaction data size")
1860+
require.Contains(t, err.Error(), "exceeds maximum")
1861+
})
1862+
1863+
t.Run("wrong chain ID attack", func(t *testing.T) {
1864+
from, fromAddr := env.NewAccountWithL2Funds()
1865+
env.accountManager.Add(from)
1866+
_, toAddr := env.NewAccountWithL2Funds()
1867+
1868+
// Create transaction with correct chain ID first, then manually modify
1869+
maliciousTx := &types.DynamicFeeTx{
1870+
ChainID: big.NewInt(int64(env.ChainID)),
1871+
Nonce: env.NonceAt(fromAddr),
1872+
GasFeeCap: env.MustGetGasPrice(),
1873+
GasTipCap: big.NewInt(10000000000),
1874+
Gas: 21000,
1875+
To: &toAddr,
1876+
Value: big.NewInt(1000),
1877+
}
1878+
1879+
signedTx, err := types.SignTx(types.NewTx(maliciousTx), env.Signer(), from)
1880+
require.NoError(t, err)
1881+
1882+
// Modify the raw bytes to have wrong chain ID after signing
1883+
// This simulates a malicious modification attempt
1884+
_, err = signedTx.MarshalBinary()
1885+
require.NoError(t, err)
1886+
1887+
// Create a new transaction with wrong chain ID and use our validation
1888+
wrongChainTx := &types.DynamicFeeTx{
1889+
ChainID: big.NewInt(999999), // Wrong chain ID
1890+
Nonce: env.NonceAt(fromAddr),
1891+
GasFeeCap: env.MustGetGasPrice(),
1892+
GasTipCap: big.NewInt(10000000000),
1893+
Gas: 21000,
1894+
To: &toAddr,
1895+
Value: big.NewInt(1000),
1896+
}
1897+
1898+
// Don't sign this one, create raw bytes manually to bypass signing validation
1899+
// This tests our post-unmarshal validation
1900+
wrongTx := types.NewTx(wrongChainTx)
1901+
wrongRawBytes, _ := wrongTx.MarshalBinary()
1902+
1903+
var txHash common.Hash
1904+
err = env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(wrongRawBytes))
1905+
require.Error(t, err)
1906+
// The error could be from signing validation or our chain ID validation
1907+
require.True(t,
1908+
strings.Contains(err.Error(), "transaction chain ID") ||
1909+
strings.Contains(err.Error(), "invalid chain id for signer"),
1910+
"Expected chain ID validation error, got: %s", err.Error())
1911+
})
1912+
1913+
t.Run("complex RLP structure attack", func(t *testing.T) {
1914+
// Create RLP with too many elements
1915+
complexRLP := make([]byte, 0, 10000)
1916+
complexRLP = append(complexRLP, 0xc0) // Start list
1917+
1918+
// Add many small elements to trigger complexity check
1919+
for i := 0; i < 1200; i++ {
1920+
complexRLP = append(complexRLP, 0x80) // Empty string
1921+
}
1922+
1923+
var txHash common.Hash
1924+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(complexRLP))
1925+
require.Error(t, err)
1926+
require.Contains(t, err.Error(), "RLP structure too complex")
1927+
})
1928+
1929+
t.Run("insufficient payload for typed transaction", func(t *testing.T) {
1930+
// Test typed transaction with insufficient payload
1931+
insufficientData := []byte{0x02} // Type 0x02 but no payload
1932+
1933+
var txHash common.Hash
1934+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(insufficientData))
1935+
require.Error(t, err)
1936+
require.Contains(t, err.Error(), "typed transaction missing payload")
1937+
})
1938+
1939+
t.Run("non-list legacy transaction", func(t *testing.T) {
1940+
// Test legacy transaction that's not an RLP list
1941+
nonListData := []byte{0x80} // RLP string, not list
1942+
1943+
var txHash common.Hash
1944+
err := env.RawClient.Call(&txHash, "eth_sendRawTransaction", hexutil.Bytes(nonListData))
1945+
require.Error(t, err)
1946+
require.Contains(t, err.Error(), "legacy transaction must be RLP list")
1947+
})
1948+
}

0 commit comments

Comments
 (0)