mirror of
https://github.com/h2non/imaginary.git
synced 2025-12-13 20:37:04 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
1
error.go
1
error.go
@@ -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
4
go.mod
@@ -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
7
go.sum
@@ -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=
|
||||
|
||||
2
image.go
2
image.go
@@ -5,10 +5,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/h2non/bimg"
|
||||
)
|
||||
|
||||
@@ -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
8
log.go
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user