diff --git a/config/config.go b/config/config.go index f539d2619..6fec8f86a 100644 --- a/config/config.go +++ b/config/config.go @@ -50,6 +50,7 @@ type Server struct { Cors Cors TrustedProxies []string SecureCookie bool + MaxBodyBytes int } type Database struct { @@ -111,6 +112,7 @@ func Get() (*Configuration, []FutureLog) { Stream: Stream{ PingPeriodSeconds: 45, }, + MaxBodyBytes: 10 * 1024 * 1024, }, Database: Database{ Dialect: "sqlite3", @@ -174,6 +176,7 @@ func Get() (*Configuration, []FutureLog) { add(parseList(&c.Server.TrustedProxies, EnvServerTrustedProxies)) add(parseBool(&c.Server.SecureCookie, EnvServerSecureCookie)) + add(parseInt(&c.Server.MaxBodyBytes, EnvServerMaxBodyBytes)) add(parseString(&c.Database.Dialect, EnvDatabaseDialect)) add(parseString(&c.Database.Connection, EnvDatabaseConnection)) diff --git a/config/keys.go b/config/keys.go index b828885a9..18be3af77 100644 --- a/config/keys.go +++ b/config/keys.go @@ -24,6 +24,7 @@ const ( EnvServerCorsAllowHeaders = "GOTIFY_SERVER_CORS_ALLOWHEADERS" EnvServerTrustedProxies = "GOTIFY_SERVER_TRUSTEDPROXIES" EnvServerSecureCookie = "GOTIFY_SERVER_SECURECOOKIE" + EnvServerMaxBodyBytes = "GOTIFY_SERVER_MAX_BODY_BYTES" EnvDatabaseDialect = "GOTIFY_DATABASE_DIALECT" EnvDatabaseConnection = "GOTIFY_DATABASE_CONNECTION" EnvDefaultUserName = "GOTIFY_DEFAULTUSER_NAME" diff --git a/gotify-server.env.example b/gotify-server.env.example index aff3ffbce..2343a6499 100644 --- a/gotify-server.env.example +++ b/gotify-server.env.example @@ -133,6 +133,12 @@ # Type: boolean # GOTIFY_SERVER_SECURECOOKIE=false +# Maximum size in bytes of a request body. Requests with a larger body are +# rejected. Set to 0 to disable the limit. +# +# Type: number +# GOTIFY_SERVER_MAX_BODY_BYTES=10485760 + # Allowed origins (regex) for cross-origin requests. Setting any CORS_* value # enables CORS handling. # diff --git a/router/router.go b/router/router.go index 89a72ca41..7f71ffaeb 100644 --- a/router/router.go +++ b/router/router.go @@ -43,6 +43,12 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co g.Use(accessLogger(), gin.Recovery(), gerror.Handler(), location.Default()) g.NoRoute(gerror.NotFound()) + if conf.Server.MaxBodyBytes > 0 { + g.Use(func(ctx *gin.Context) { + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, int64(conf.Server.MaxBodyBytes)) + }) + } + if conf.Server.SSL.Enabled && conf.Server.SSL.RedirectToHTTPS { g.Use(func(ctx *gin.Context) { if ctx.Request.TLS != nil { diff --git a/router/router_test.go b/router/router_test.go index 9ef4d040f..d4dbb8a89 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -43,6 +43,7 @@ func (s *IntegrationSuite) BeforeTest(string, string) { s.db.GormDatabase, &model.VersionInfo{Version: "1.0.0", BuildDate: "2018-02-20-17:30:47", Commit: "asdasds"}, &config.Configuration{ + Server: config.Server{MaxBodyBytes: 1024}, PassStrength: 5, LocalAuthEnabled: true, OIDC: config.OIDC{IDPName: "Company XYZ SSO"}, @@ -361,6 +362,22 @@ func (s *IntegrationSuite) TestSendMessage() { assert.Equal(s.T(), token.ID, msg.ApplicationID) } +func (s *IntegrationSuite) TestMaxBodyBytes() { + s.db.User(2).AppWithToken(1, "Aapp") + + // 1024 bytes, exactly the configured limit. + req := s.newRequest("POST", "message", `{"message":"`+strings.Repeat("a", 1010)+`"}`) + req.Header.Add("X-Gotify-Key", "Aapp") + res, err := client.Do(req) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 200, res.StatusCode) + + // 1025 bytes, one byte over the limit. + req = s.newRequest("POST", "message", `{"message":"`+strings.Repeat("a", 1011)+`"}`) + req.Header.Add("X-Gotify-Key", "Aapp") + doRequestAndExpect(s.T(), req, 413, `{"error":"Request Entity Too Large", "errorCode":413, "errorDescription":"http: request body too large"}`) +} + func (s *IntegrationSuite) TestPluginLoadFail_expectPanic() { db := testdb.NewDBWithDefaultUser(s.T()) defer db.Close()