-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
To provide some safety when linking to user-supplied external images, we provide a simple image proxy handler. Images accessed through this proxy will only be served if they meet the following criteria: - Appear to be valid image files - Are in a permitted format: GIF, JPEG, PNG or WebP - Do not have an excessive width or height (5000 pixels max, by default) To serve an image through this proxy, its URL should be passed to the handler's path as a `src` query param. The path is supplied to the application in the `IMAGE_PROXY_PATH` environment variable. We also provide a helper method to make forming the proxy links easier: Thruster.image_proxy_path('https://example.com/image.jpg')
- Loading branch information
1 parent
8b3b83f
commit eda3d10
Showing
17 changed files
with
286 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package internal | ||
|
||
import ( | ||
"bytes" | ||
"image" | ||
"io" | ||
"log/slog" | ||
"net/http" | ||
"net/url" | ||
"slices" | ||
"time" | ||
|
||
_ "image/gif" | ||
_ "image/jpeg" | ||
_ "image/png" | ||
|
||
_ "golang.org/x/image/webp" | ||
) | ||
|
||
var allowedFormats = []string{"gif", "jpeg", "png", "webp"} | ||
|
||
const ( | ||
imageProxyHandlerPath = "/_t/image" | ||
imageProxyMaxDimension = 5000 | ||
) | ||
|
||
type ImageProxyHandler struct { | ||
httpClient *http.Client | ||
} | ||
|
||
func RegisterNewImageProxyHandler(mux *http.ServeMux) { | ||
handler := &ImageProxyHandler{ | ||
httpClient: &http.Client{ | ||
Timeout: 10 * time.Second, | ||
}, | ||
} | ||
|
||
mux.Handle("GET "+imageProxyHandlerPath, handler) | ||
} | ||
|
||
func (h *ImageProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
remoteURL := h.extractRemoteURL(r) | ||
if remoteURL == nil { | ||
http.Error(w, "invalid url", http.StatusNotFound) | ||
return | ||
} | ||
|
||
resp, err := h.httpClient.Get(remoteURL.String()) | ||
if err != nil { | ||
http.Error(w, "error fetching remote image", http.StatusBadGateway) | ||
return | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
h.copyHeaders(w, resp) | ||
w.WriteHeader(resp.StatusCode) | ||
return | ||
} | ||
|
||
imageReader := h.sanitizeImage(resp.Body) | ||
if imageReader == nil { | ||
http.Error(w, "invalid image", http.StatusForbidden) | ||
return | ||
} | ||
|
||
slog.Info("Proxying remote image", "url", remoteURL) | ||
|
||
h.copyHeaders(w, resp) | ||
w.WriteHeader(http.StatusOK) | ||
io.Copy(w, imageReader) | ||
} | ||
|
||
// Private | ||
|
||
func (h *ImageProxyHandler) extractRemoteURL(r *http.Request) *url.URL { | ||
urlString := r.URL.Query().Get("src") | ||
if urlString == "" { | ||
return nil | ||
} | ||
|
||
remoteURL, err := url.Parse(urlString) | ||
if err != nil || (remoteURL.Scheme != "http" && remoteURL.Scheme != "https") { | ||
return nil | ||
} | ||
|
||
return remoteURL | ||
} | ||
|
||
func (h *ImageProxyHandler) copyHeaders(w http.ResponseWriter, resp *http.Response) { | ||
for k, v := range resp.Header { | ||
w.Header()[k] = v | ||
} | ||
} | ||
|
||
func (h *ImageProxyHandler) sanitizeImage(f io.Reader) io.Reader { | ||
var buf bytes.Buffer | ||
reader := io.TeeReader(f, &buf) | ||
|
||
cfg, format, err := image.DecodeConfig(reader) | ||
if err != nil { | ||
slog.Debug("ImageProxy: image format not valid", "err", err) | ||
return nil | ||
} | ||
|
||
if !slices.Contains(allowedFormats, format) { | ||
slog.Debug("ImageProxy: image format not allowed", "format", format) | ||
return nil | ||
} | ||
|
||
if cfg.Width > imageProxyMaxDimension || cfg.Height > imageProxyMaxDimension { | ||
slog.Debug("ImageProxy: image too large", "width", cfg.Width, "height", cfg.Height) | ||
return nil | ||
} | ||
|
||
slog.Debug("ImageProxy: image acceptable", "format", format, "width", cfg.Width, "height", cfg.Height) | ||
return io.MultiReader(&buf, f) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package internal | ||
|
||
import ( | ||
"image" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestImageProxy_serving_valid_images(t *testing.T) { | ||
tests := map[string]struct { | ||
filename string | ||
statusCode int | ||
}{ | ||
"valid gif": {"image.gif", http.StatusOK}, | ||
"valid jpg": {"image.jpg", http.StatusOK}, | ||
"valid png": {"image.png", http.StatusOK}, | ||
"valid webp": {"image.webp", http.StatusOK}, | ||
"valid svg": {"image.svg", http.StatusForbidden}, | ||
"not an image": {"loremipsum.txt", http.StatusForbidden}, | ||
"missing file": {"doesnotexist.txt", http.StatusNotFound}, | ||
} | ||
|
||
for name, tc := range tests { | ||
t.Run(name, func(t *testing.T) { | ||
remoteServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if !fixtureExists(tc.filename) { | ||
w.WriteHeader(http.StatusNotFound) | ||
return | ||
} | ||
|
||
w.Write(fixtureContent(tc.filename)) | ||
})) | ||
defer remoteServer.Close() | ||
|
||
mux := http.NewServeMux() | ||
RegisterNewImageProxyHandler(mux) | ||
localServer := httptest.NewServer(mux) | ||
defer localServer.Close() | ||
|
||
imageURL, _ := url.Parse(localServer.URL + imageProxyHandlerPath) | ||
params := url.Values{} | ||
params.Add("src", remoteServer.URL) | ||
imageURL.RawQuery = params.Encode() | ||
|
||
resp, err := http.Get(imageURL.String()) | ||
|
||
require.NoError(t, err) | ||
assert.Equal(t, tc.statusCode, resp.StatusCode) | ||
|
||
if tc.statusCode == http.StatusOK { | ||
_, _, err = image.Decode(resp.Body) | ||
require.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ module Thruster | |
end | ||
|
||
require_relative "thruster/version" | ||
require_relative "thruster/helpers" |
Oops, something went wrong.