Decompression exploit check (#404)

* Bump bimg version to 1.1.7

* Add decompression bomb exploit check

* Update README with new flag

* Fix tests
This commit is contained in:
SeaaaaaSharp
2022-12-03 23:00:34 +00:00
committed by GitHub
parent cfbf8d724c
commit 35c87ba9d5
10 changed files with 67 additions and 47 deletions

View File

@@ -347,6 +347,7 @@ Options:
-url-signature-key The URL signature key (32 characters minimum)
-allowed-origins <urls> Restrict remote image source processing to certain origins (separated by commas). Note: Origins are validated against host *AND* path.
-max-allowed-size <bytes> Restrict maximum size of http image source (in bytes)
-max-allowed-resolution <megapixels> Restrict maximum resolution of the image [default: 18.0]
-certfile <path> TLS certificate file path
-keyfile <path> TLS private key file path
-authorization <value> Defines a constant Authorization header value passed to all the image source servers. -enable-url-source flag must be defined. This overwrites authorization headers forwarding behavior via X-Forward-Authorization

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"mime"
"path"
"net/http"
"path"
"strconv"
"strings"
@@ -16,14 +16,14 @@ import (
func indexController(o ServerOptions) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != path.Join(o.PathPrefix, "/") {
ErrorReply(r, w, ErrNotFound, ServerOptions{})
return
ErrorReply(r, w, ErrNotFound, ServerOptions{})
return
}
body, _ := json.Marshal(Versions{
Version,
bimg.Version,
bimg.VipsVersion,
Version,
bimg.Version,
bimg.VipsVersion,
})
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(body)
@@ -120,6 +120,21 @@ func imageHandler(w http.ResponseWriter, r *http.Request, buf []byte, operation
return
}
sizeInfo, err := bimg.Size(buf)
if err != nil {
ErrorReply(r, w, NewError("Error while processing the image: "+err.Error(), http.StatusBadRequest), o)
return
}
// https://en.wikipedia.org/wiki/Image_resolution#Pixel_count
imgResolution := float64(sizeInfo.Width) * float64(sizeInfo.Height)
if (imgResolution / 1000000) > o.MaxAllowedPixels {
ErrorReply(r, w, ErrResolutionTooBig, o)
return
}
image, err := operation.Run(buf, opts)
if err != nil {
// Ensure the Vary header is set when an error occurs
@@ -149,34 +164,34 @@ func imageHandler(w http.ResponseWriter, r *http.Request, buf []byte, operation
func formController(o ServerOptions) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
operations := []struct {
name string
method string
args string
name string
method string
args string
}{
{"Resize", "resize", "width=300&height=200&type=jpeg"},
{"Force resize", "resize", "width=300&height=200&force=true"},
{"Crop", "crop", "width=300&quality=95"},
{"SmartCrop", "crop", "width=300&height=260&quality=95&gravity=smart"},
{"Extract", "extract", "top=100&left=100&areawidth=300&areaheight=150"},
{"Enlarge", "enlarge", "width=1440&height=900&quality=95"},
{"Rotate", "rotate", "rotate=180"},
{"AutoRotate", "autorotate", "quality=90"},
{"Flip", "flip", ""},
{"Flop", "flop", ""},
{"Thumbnail", "thumbnail", "width=100"},
{"Zoom", "zoom", "factor=2&areawidth=300&top=80&left=80"},
{"Color space (black&white)", "resize", "width=400&height=300&colorspace=bw"},
{"Add watermark", "watermark", "textwidth=100&text=Hello&font=sans%2012&opacity=0.5&color=255,200,50"},
{"Convert format", "convert", "type=png"},
{"Image metadata", "info", ""},
{"Gaussian blur", "blur", "sigma=15.0&minampl=0.2"},
{"Pipeline (image reduction via multiple transformations)", "pipeline", "operations=%5B%7B%22operation%22:%20%22crop%22,%20%22params%22:%20%7B%22width%22:%20300,%20%22height%22:%20260%7D%7D,%20%7B%22operation%22:%20%22convert%22,%20%22params%22:%20%7B%22type%22:%20%22webp%22%7D%7D%5D"},
{"Resize", "resize", "width=300&height=200&type=jpeg"},
{"Force resize", "resize", "width=300&height=200&force=true"},
{"Crop", "crop", "width=300&quality=95"},
{"SmartCrop", "crop", "width=300&height=260&quality=95&gravity=smart"},
{"Extract", "extract", "top=100&left=100&areawidth=300&areaheight=150"},
{"Enlarge", "enlarge", "width=1440&height=900&quality=95"},
{"Rotate", "rotate", "rotate=180"},
{"AutoRotate", "autorotate", "quality=90"},
{"Flip", "flip", ""},
{"Flop", "flop", ""},
{"Thumbnail", "thumbnail", "width=100"},
{"Zoom", "zoom", "factor=2&areawidth=300&top=80&left=80"},
{"Color space (black&white)", "resize", "width=400&height=300&colorspace=bw"},
{"Add watermark", "watermark", "textwidth=100&text=Hello&font=sans%2012&opacity=0.5&color=255,200,50"},
{"Convert format", "convert", "type=png"},
{"Image metadata", "info", ""},
{"Gaussian blur", "blur", "sigma=15.0&minampl=0.2"},
{"Pipeline (image reduction via multiple transformations)", "pipeline", "operations=%5B%7B%22operation%22:%20%22crop%22,%20%22params%22:%20%7B%22width%22:%20300,%20%22height%22:%20260%7D%7D,%20%7B%22operation%22:%20%22convert%22,%20%22params%22:%20%7B%22type%22:%20%22webp%22%7D%7D%5D"},
}
html := "<html><body>"
for _, form := range operations {
html += fmt.Sprintf(`
html += fmt.Sprintf(`
<h1>%s</h1>
<form method="POST" action="%s?%s" enctype="multipart/form-data">
<input type="file" name="file" />

View File

@@ -24,6 +24,7 @@ var (
ErrNotImplemented = NewError("Not implemented endpoint", http.StatusNotImplemented)
ErrInvalidURLSignature = NewError("Invalid URL signature", http.StatusBadRequest)
ErrURLSignatureMismatch = NewError("URL signature mismatch", http.StatusForbidden)
ErrResolutionTooBig = NewError("Image resolution is too big", http.StatusUnprocessableEntity)
)
type Error struct {

4
go.mod
View File

@@ -4,9 +4,9 @@ go 1.12
require (
github.com/garyburd/redigo v1.6.0 // indirect
github.com/h2non/bimg v1.1.7
github.com/h2non/filetype v1.1.0
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3
github.com/h2non/bimg v1.1.4
github.com/h2non/filetype v1.1.0
gopkg.in/throttled/throttled.v2 v2.0.3
)

7
go.sum
View File

@@ -1,8 +1,7 @@
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/h2non/bimg v1.1.2 h1:J75W2eM5FT0KjcwsL2aiy1Ilu0Xy0ENb0sU+HHUJAvw=
github.com/h2non/bimg v1.1.2/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/h2non/bimg v1.1.4 h1:6qf7qDo3d9axbNUOcSoQmzleBCMTcQ1PwF3FgGhX4O0=
github.com/h2non/bimg v1.1.4/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/h2non/bimg v1.1.7 h1:JKJe70nDNMWp2wFnTLMGB8qJWQQMaKRn56uHmC/4+34=
github.com/h2non/bimg v1.1.7/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA=
github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=

View File

@@ -5,10 +5,10 @@ import (
"errors"
"fmt"
"io"
"strings"
"io/ioutil"
"math"
"net/http"
"strings"
"github.com/h2non/bimg"
)

View File

@@ -33,6 +33,7 @@ var (
aURLSignatureKey = flag.String("url-signature-key", "", "The URL signature key (32 characters minimum)")
aAllowedOrigins = flag.String("allowed-origins", "", "Restrict remote image source processing to certain origins (separated by commas). Note: Origins are validated against host *AND* path.")
aMaxAllowedSize = flag.Int("max-allowed-size", 0, "Restrict maximum size of http image source (in bytes)")
aMaxAllowedPixels = flag.Float64("max-allowed-resolution", 18.0, "Restrict maximum resolution of the image (in megapixels)")
aKey = flag.String("key", "", "Define API key for authorization")
aMount = flag.String("mount", "", "Mount server local directory")
aCertFile = flag.String("certfile", "", "TLS certificate file path")
@@ -95,6 +96,7 @@ Options:
-url-signature-key The URL signature key (32 characters minimum)
-allowed-origins <urls> Restrict remote image source processing to certain origins (separated by commas)
-max-allowed-size <bytes> Restrict maximum size of http image source (in bytes)
-max-allowed-resolution <megapixels> Restrict maximum resolution of the image [default: 18.0]
-certfile <path> TLS certificate file path
-keyfile <path> TLS private key file path
-authorization <value> Defines a constant Authorization header value passed to all the image source servers. -enable-url-source flag must be defined. This overwrites authorization headers forwarding behavior via X-Forward-Authorization
@@ -158,6 +160,7 @@ func main() {
ForwardHeaders: parseForwardHeaders(*aForwardHeaders),
AllowedOrigins: parseOrigins(*aAllowedOrigins),
MaxAllowedSize: *aMaxAllowedSize,
MaxAllowedPixels: *aMaxAllowedPixels,
LogLevel: getLogLevel(*aLogLevel),
ReturnSize: *aReturnSize,
}

8
log.go
View File

@@ -44,9 +44,9 @@ func (r *LogRecord) WriteHeader(status int) {
// LogHandler maps the HTTP handler with a custom io.Writer compatible stream
type LogHandler struct {
handler http.Handler
io io.Writer
logLevel string
handler http.Handler
io io.Writer
logLevel string
}
// NewLog creates a new logger
@@ -79,7 +79,7 @@ func (h *LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
record.time = finishTime.UTC()
record.elapsedTime = finishTime.Sub(startTime)
switch h.logLevel{
switch h.logLevel {
case "error":
if record.status >= http.StatusInternalServerError {
record.Log(h.io)

View File

@@ -2,15 +2,15 @@ package main
import (
"context"
"log"
"net/http"
"net/url"
"log"
"os"
"os/signal"
"syscall"
"path"
"strconv"
"strings"
"syscall"
"time"
)
@@ -22,6 +22,7 @@ type ServerOptions struct {
HTTPReadTimeout int
HTTPWriteTimeout int
MaxAllowedSize int
MaxAllowedPixels float64
CORS bool
Gzip bool // deprecated
AuthForwarding bool

View File

@@ -16,7 +16,7 @@ import (
)
func TestIndex(t *testing.T) {
opts := ServerOptions{PathPrefix: "/"}
opts := ServerOptions{PathPrefix: "/", MaxAllowedPixels: 18.0}
ts := testServer(indexController(opts))
defer ts.Close()
@@ -275,7 +275,7 @@ func TestFit(t *testing.T) {
}
func TestRemoteHTTPSource(t *testing.T) {
opts := ServerOptions{EnableURLSource: true}
opts := ServerOptions{EnableURLSource: true, MaxAllowedPixels: 18.0}
fn := ImageMiddleware(opts)(Crop)
LoadSources(opts)
@@ -316,7 +316,7 @@ func TestRemoteHTTPSource(t *testing.T) {
}
func TestInvalidRemoteHTTPSource(t *testing.T) {
opts := ServerOptions{EnableURLSource: true}
opts := ServerOptions{EnableURLSource: true, MaxAllowedPixels: 18.0}
fn := ImageMiddleware(opts)(Crop)
LoadSources(opts)
@@ -339,7 +339,7 @@ func TestInvalidRemoteHTTPSource(t *testing.T) {
}
func TestMountDirectory(t *testing.T) {
opts := ServerOptions{Mount: "testdata"}
opts := ServerOptions{Mount: "testdata", MaxAllowedPixels: 18.0}
fn := ImageMiddleware(opts)(Crop)
LoadSources(opts)
@@ -374,7 +374,7 @@ func TestMountDirectory(t *testing.T) {
}
func TestMountInvalidDirectory(t *testing.T) {
fn := ImageMiddleware(ServerOptions{Mount: "_invalid_"})(Crop)
fn := ImageMiddleware(ServerOptions{Mount: "_invalid_", MaxAllowedPixels: 18.0})(Crop)
ts := httptest.NewServer(fn)
url := ts.URL + "?top=100&left=100&areawidth=200&areaheight=120&file=large.jpg"
defer ts.Close()
@@ -408,7 +408,7 @@ func TestMountInvalidPath(t *testing.T) {
func controller(op Operation) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
buf, _ := ioutil.ReadAll(r.Body)
imageHandler(w, r, buf, op, ServerOptions{})
imageHandler(w, r, buf, op, ServerOptions{MaxAllowedPixels: 18.0})
}
}