Skip to content

Commit

Permalink
Merge pull request #5050 from tamirms/internal-fixes
Browse files Browse the repository at this point in the history
horizon: Merge internal fixes into master
  • Loading branch information
tamirms authored Sep 13, 2023
2 parents 176a6f4 + dfa1d58 commit b19a4ce
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 2 deletions.
70 changes: 70 additions & 0 deletions gxdr/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package gxdr

import (
"encoding/base64"
"strings"

goxdr "github.com/xdrpp/goxdr/xdr"
)

const DefaultMaxDepth = 500

type depthLimiter struct {
depth int
maxDepth int
decoder *goxdr.XdrIn
}

func (*depthLimiter) Sprintf(f string, args ...interface{}) string {
return ""
}

func (d *depthLimiter) Marshal(field string, i goxdr.XdrType) {
switch t := goxdr.XdrBaseType(i).(type) {
case goxdr.XdrAggregate:
if d.depth > d.maxDepth {
goxdr.XdrPanic("max depth of %d exceeded", d.maxDepth)
}
d.depth++
t.XdrRecurse(d, field)
d.depth--
default:
d.decoder.Marshal(field, t)
}
}

// ValidateTransactionEnvelope validates the given transaction envelope
// to make sure that it does not contain malicious arrays or nested
// structures which are too deep
func ValidateTransactionEnvelope(b64Envelope string, maxDepth int) error {
return validate(b64Envelope, &TransactionEnvelope{}, maxDepth)
}

// ValidateLedgerKey validates the given ledger key
// to make sure that it does not contain malicious arrays or nested
// structures which are too deep
func ValidateLedgerKey(b64Key string, maxDepth int) error {
return validate(b64Key, &LedgerKey{}, maxDepth)
}

func validate(b64 string, val goxdr.XdrType, maxDepth int) (err error) {
d := &depthLimiter{
depth: 0,
maxDepth: maxDepth,
decoder: &goxdr.XdrIn{
In: base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64)),
},
}

defer func() {
switch i := recover().(type) {
case nil:
case goxdr.XdrError:
err = i
default:
panic(i)
}
}()
val.XdrMarshal(d, "")
return nil
}
99 changes: 99 additions & 0 deletions gxdr/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package gxdr

import (
"encoding/base64"
"testing"

"github.com/stretchr/testify/assert"
goxdr "github.com/xdrpp/goxdr/xdr"
)

func buildVec(depth int) SCVal {
if depth <= 0 {
symbol := SCSymbol("s")
return SCVal{
Type: SCV_SYMBOL,
_u: &symbol,
}
}
vec := &SCVec{
buildVec(depth - 1),
}
return SCVal{Type: SCV_VEC, _u: &vec}
}

func buildMaliciousVec(t *testing.T) string {
vals := &SCVec{}
for i := 0; i < 0x0D; i++ {
symbol := SCSymbol("s")
*vals = append(*vals, SCVal{
Type: SCV_SYMBOL,
_u: &symbol,
})
}
vec := SCVal{Type: SCV_VEC, _u: &vals}
raw := Dump(&vec)
// raw[8-11] represents the part of the xdr that holds the
// length of the vector
for i, b := range raw {
if b == 0x0D {
assert.Equal(t, 11, i)
}
}
// here we override the most significant byte in the vector length
// so that the vector length in the xdr is 0xFA00000D which
// is equal to 4194304013
raw[8] = 0xFA
return base64.StdEncoding.EncodeToString(raw)
}

func TestValidator(t *testing.T) {
shallowVec := buildVec(2)
deepVec := buildVec(100)
for _, testCase := range []struct {
name string
input string
maxDepth int
val goxdr.XdrType
expectedError string
}{
{
"invalid base 64 input",
"{}<>~!@$#",
500,
&LedgerEntry{},
"illegal base64 data at input byte 0",
},
{
"valid depth",
base64.StdEncoding.EncodeToString(Dump(&shallowVec)),
500,
&SCVal{},
"",
},
{
"invalid depth",
base64.StdEncoding.EncodeToString(Dump(&deepVec)),
50,
&SCVal{},
"max depth of 50 exceeded",
},
{
"malicious length",
buildMaliciousVec(t),
500,
&SCVal{},
"EOF",
},
} {
t.Run(testCase.name, func(t *testing.T) {
err := validate(testCase.input, testCase.val, testCase.maxDepth)
if testCase.expectedError == "" {
assert.NoError(t, err)
assert.Equal(t, testCase.input, base64.StdEncoding.EncodeToString(Dump(testCase.val)))
} else {
assert.EqualError(t, err, testCase.expectedError)
}
})
}
}
4 changes: 4 additions & 0 deletions services/horizon/internal/actions/submit_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"mime"
"net/http"

"github.com/stellar/go/gxdr"
"github.com/stellar/go/network"
"github.com/stellar/go/protocols/horizon"
hProblem "github.com/stellar/go/services/horizon/internal/render/problem"
Expand Down Expand Up @@ -37,6 +38,9 @@ type envelopeInfo struct {

func extractEnvelopeInfo(raw string, passphrase string) (envelopeInfo, error) {
result := envelopeInfo{raw: raw}
if err := gxdr.ValidateTransactionEnvelope(raw, gxdr.DefaultMaxDepth); err != nil {
return result, err
}
err := xdr.SafeUnmarshalBase64(raw, &result.parsed)
if err != nil {
return result, err
Expand Down
1 change: 1 addition & 0 deletions services/horizon/internal/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ func (a *App) init() error {
SSEUpdateFrequency: a.config.SSEUpdateFrequency,
StaleThreshold: a.config.StaleThreshold,
ConnectionTimeout: a.config.ConnectionTimeout,
MaxHTTPRequestSize: a.config.MaxHTTPRequestSize,
NetworkPassphrase: a.config.NetworkPassphrase,
MaxPathLength: a.config.MaxPathLength,
MaxAssetsPerPathRequest: a.config.MaxAssetsPerPathRequest,
Expand Down
2 changes: 2 additions & 0 deletions services/horizon/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type Config struct {

SSEUpdateFrequency time.Duration
ConnectionTimeout time.Duration
// MaxHTTPRequestSize is the maximum allowed request payload size
MaxHTTPRequestSize uint
RateQuota *throttled.RateQuota
FriendbotURL *url.URL
LogLevel logrus.Level
Expand Down
12 changes: 11 additions & 1 deletion services/horizon/internal/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/viper"

"github.com/stellar/throttled"

"github.com/stellar/go/ingest/ledgerbackend"
"github.com/stellar/go/network"
"github.com/stellar/go/services/horizon/internal/db2/schema"
Expand All @@ -20,7 +22,6 @@ import (
"github.com/stellar/go/support/db"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/support/log"
"github.com/stellar/throttled"
)

const (
Expand Down Expand Up @@ -62,6 +63,8 @@ const (
StellarPubnet = "pubnet"
// StellarTestnet is a constant representing the Stellar test network
StellarTestnet = "testnet"

defaultMaxHTTPRequestSize = uint(200 * 1024)
)

// validateBothOrNeither ensures that both options are provided, if either is provided.
Expand Down Expand Up @@ -366,6 +369,13 @@ func Flags() (*Config, support.ConfigOptions) {
CustomSetValue: support.SetDuration,
Usage: "defines the timeout of connection after which 504 response will be sent or stream will be closed, if Horizon is behind a load balancer with idle connection timeout, this should be set to a few seconds less that idle timeout, does not apply to POST /transactions",
},
&support.ConfigOption{
Name: "max-http-request-size",
ConfigKey: &config.MaxHTTPRequestSize,
OptType: types.Uint,
FlagDefault: defaultMaxHTTPRequestSize,
Usage: "sets the limit on the maximum allowed http request payload size, default is 200kb, to disable the limit check, set to 0, only do so if you acknowledge the implications of accepting unbounded http request payload sizes.",
},
&support.ConfigOption{
Name: "per-hour-rate-limit",
ConfigKey: &config.RateQuota,
Expand Down
6 changes: 6 additions & 0 deletions services/horizon/internal/httpx/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type RouterConfig struct {
SSEUpdateFrequency time.Duration
StaleThreshold uint
ConnectionTimeout time.Duration
MaxHTTPRequestSize uint
NetworkPassphrase string
MaxPathLength uint
MaxAssetsPerPathRequest int
Expand Down Expand Up @@ -89,6 +90,11 @@ func (r *Router) addMiddleware(config *RouterConfig,
}))
r.Use(loggerMiddleware(serverMetrics))
r.Use(timeoutMiddleware(config.ConnectionTimeout))
if config.MaxHTTPRequestSize > 0 {
r.Use(func(handler http.Handler) http.Handler {
return http.MaxBytesHandler(handler, int64(config.MaxHTTPRequestSize))
})
}
r.Use(recoverMiddleware)
r.Use(chimiddleware.Compress(flate.DefaultCompression, "application/hal+json"))

Expand Down
44 changes: 43 additions & 1 deletion services/horizon/internal/integration/txsub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package integration
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/services/horizon/internal/test/integration"
"github.com/stellar/go/txnbuild"
"github.com/stretchr/testify/assert"
)

func TestTxSub(t *testing.T) {
Expand Down Expand Up @@ -50,3 +53,42 @@ func TestTxSub(t *testing.T) {
assert.Error(t, err)
})
}

func TestTxSubLimitsBodySize(t *testing.T) {
if integration.GetCoreMaxSupportedProtocol() < 20 {
t.Skip("This test run does not support less than Protocol 20")
}

itest := integration.NewTest(t, integration.Config{
ProtocolVersion: 20,
EnableSorobanRPC: true,
HorizonEnvironment: map[string]string{
"MAX_HTTP_REQUEST_SIZE": "1800",
},
})

// establish which account will be contract owner, and load it's current seq
sourceAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{
AccountID: itest.Master().Address(),
})
require.NoError(t, err)

installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), "soroban_sac_test.wasm")
preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp)
_, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp)
assert.EqualError(
t, err,
"horizon error: \"Transaction Malformed\" - check horizon.Error.Problem for more information",
)

sourceAccount, err = itest.Client().AccountDetail(horizonclient.AccountRequest{
AccountID: itest.Master().Address(),
})
require.NoError(t, err)

installContractOp = assembleInstallContractCodeOp(t, itest.Master().Address(), "soroban_add_u64.wasm")
preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *installContractOp)
tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp)
require.NoError(t, err)
require.True(t, tx.Successful)
}

0 comments on commit b19a4ce

Please sign in to comment.