1
0
mirror of https://github.com/labstack/echo.git synced 2026-05-16 09:48:24 +02:00
Files
echo/middleware/decompress.go
T
toimtoimtoim f071367e3c V5 changes
2026-01-18 18:14:41 +02:00

156 lines
4.6 KiB
Go

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware
import (
"compress/gzip"
"io"
"net/http"
"sync"
"github.com/labstack/echo/v5"
)
// DecompressConfig defines the config for Decompress middleware.
type DecompressConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers
GzipDecompressPool Decompressor
// MaxDecompressedSize limits the maximum size of decompressed request body in bytes.
// If the decompressed body exceeds this limit, the middleware returns HTTP 413 error.
// This prevents zip bomb attacks where small compressed payloads decompress to huge sizes.
// Default: 100 * MB (104,857,600 bytes)
// Set to -1 to disable limits (not recommended in production).
MaxDecompressedSize int64
}
// GZIPEncoding content-encoding header if set to "gzip", decompress body contents.
const GZIPEncoding string = "gzip"
// Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers
type Decompressor interface {
gzipDecompressPool() sync.Pool
}
// DefaultGzipDecompressPool is the default implementation of Decompressor interface
type DefaultGzipDecompressPool struct {
}
func (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool {
return sync.Pool{New: func() any { return new(gzip.Reader) }}
}
// Decompress decompresses request body based if content encoding type is set to "gzip" with default config
//
// SECURITY: By default, this limits decompressed data to 100MB to prevent zip bomb attacks.
// To customize the limit, use DecompressWithConfig. To disable limits (not recommended in production),
// set MaxDecompressedSize to -1.
func Decompress() echo.MiddlewareFunc {
return DecompressWithConfig(DecompressConfig{})
}
// DecompressWithConfig returns a decompress middleware with config or panics on invalid configuration.
//
// SECURITY: If MaxDecompressedSize is not set (zero value), it defaults to 100MB to prevent
// DoS attacks via zip bombs. Set to -1 to explicitly disable limits if needed for your use case.
func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {
return toMiddlewareOrPanic(config)
}
// ToMiddleware converts DecompressConfig to middleware or returns an error for invalid configuration
func (config DecompressConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
if config.Skipper == nil {
config.Skipper = DefaultSkipper
}
if config.GzipDecompressPool == nil {
config.GzipDecompressPool = &DefaultGzipDecompressPool{}
}
// Apply secure default for decompression limit
if config.MaxDecompressedSize == 0 {
config.MaxDecompressedSize = 100 * MB
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
pool := config.GzipDecompressPool.gzipDecompressPool()
return func(c *echo.Context) error {
if config.Skipper(c) {
return next(c)
}
if c.Request().Header.Get(echo.HeaderContentEncoding) != GZIPEncoding {
return next(c)
}
i := pool.Get()
gr, ok := i.(*gzip.Reader)
if !ok || gr == nil {
if err, isErr := i.(error); isErr {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return echo.NewHTTPError(http.StatusInternalServerError, "unexpected type from gzip decompression pool")
}
defer pool.Put(gr)
b := c.Request().Body
defer b.Close()
if err := gr.Reset(b); err != nil {
if err == io.EOF { //ignore if body is empty
return next(c)
}
return err
}
// only Close gzip reader if it was set to a proper gzip source otherwise it will panic on close.
defer gr.Close()
// Apply decompression size limit to prevent zip bombs
if config.MaxDecompressedSize > 0 {
c.Request().Body = &limitedGzipReader{
Reader: gr,
remaining: config.MaxDecompressedSize,
limit: config.MaxDecompressedSize,
}
} else {
// -1 means explicitly unlimited (not recommended)
c.Request().Body = gr
}
return next(c)
}
}, nil
}
// limitedGzipReader wraps a gzip reader with size limiting to prevent zip bombs
type limitedGzipReader struct {
*gzip.Reader
remaining int64
limit int64
}
func (r *limitedGzipReader) Read(p []byte) (n int, err error) {
if r.remaining <= 0 {
// Limit exceeded - return 413 error
return 0, echo.ErrStatusRequestEntityTooLarge
}
// Limit the read to remaining bytes
if int64(len(p)) > r.remaining {
p = p[:r.remaining]
}
n, err = r.Reader.Read(p)
r.remaining -= int64(n)
return n, err
}
func (r *limitedGzipReader) Close() error {
return r.Reader.Close()
}