Fix invalid parsing of data URL
Fetching icons crashes with "slice bounds out of range" error if no encoding is specified.
This commit is contained in:
parent
40f958c5ef
commit
f6825c1c60
2 changed files with 77 additions and 17 deletions
|
@ -10,6 +10,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
stdlib_url "net/url"
|
||||||
|
|
||||||
"miniflux.app/config"
|
"miniflux.app/config"
|
||||||
"miniflux.app/crypto"
|
"miniflux.app/crypto"
|
||||||
"miniflux.app/http/client"
|
"miniflux.app/http/client"
|
||||||
|
@ -130,40 +132,63 @@ func downloadIcon(iconURL, userAgent string, fetchViaProxy, allowSelfSignedCerti
|
||||||
return icon, nil
|
return icon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs#syntax
|
||||||
|
// data:[<mediatype>][;base64],<data>
|
||||||
func parseImageDataURL(value string) (*model.Icon, error) {
|
func parseImageDataURL(value string) (*model.Icon, error) {
|
||||||
colon := strings.Index(value, ":")
|
var mediaType string
|
||||||
semicolon := strings.Index(value, ";")
|
var encoding string
|
||||||
|
|
||||||
|
if !strings.HasPrefix(value, "data:") {
|
||||||
|
return nil, fmt.Errorf(`icon: invalid data URL (missing data:) %q`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value[5:]
|
||||||
|
|
||||||
comma := strings.Index(value, ",")
|
comma := strings.Index(value, ",")
|
||||||
|
if comma < 0 {
|
||||||
if colon <= 0 || semicolon <= 0 || comma <= 0 {
|
return nil, fmt.Errorf(`icon: invalid data URL (no comma) %q`, value)
|
||||||
return nil, fmt.Errorf(`icon: invalid data url "%s"`, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeType := value[colon+1 : semicolon]
|
|
||||||
encoding := value[semicolon+1 : comma]
|
|
||||||
data := value[comma+1:]
|
data := value[comma+1:]
|
||||||
|
semicolon := strings.Index(value[0:comma], ";")
|
||||||
|
|
||||||
if encoding != "base64" {
|
if semicolon > 0 {
|
||||||
return nil, fmt.Errorf(`icon: unsupported data url encoding "%s"`, value)
|
mediaType = value[0:semicolon]
|
||||||
|
encoding = value[semicolon+1 : comma]
|
||||||
|
} else {
|
||||||
|
mediaType = value[0:comma]
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(mimeType, "image/") {
|
if !strings.HasPrefix(mediaType, "image/") {
|
||||||
return nil, fmt.Errorf(`icon: invalid mime type "%s"`, mimeType)
|
return nil, fmt.Errorf(`icon: invalid media type %q`, mediaType)
|
||||||
}
|
}
|
||||||
|
|
||||||
blob, err := base64.StdEncoding.DecodeString(data)
|
var blob []byte
|
||||||
if err != nil {
|
switch encoding {
|
||||||
return nil, fmt.Errorf(`icon: invalid data "%s" (%v)`, value, err)
|
case "base64":
|
||||||
|
var err error
|
||||||
|
blob, err = base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`icon: invalid data %q (%v)`, value, err)
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
decodedData, err := stdlib_url.QueryUnescape(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`icon: unable to decode data URL %q`, value)
|
||||||
|
}
|
||||||
|
blob = []byte(decodedData)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf(`icon: unsupported data URL encoding %q`, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(blob) == 0 {
|
if len(blob) == 0 {
|
||||||
return nil, fmt.Errorf(`icon: empty data "%s"`, value)
|
return nil, fmt.Errorf(`icon: empty data URL %q`, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
icon := &model.Icon{
|
icon := &model.Icon{
|
||||||
Hash: crypto.HashFromBytes(blob),
|
Hash: crypto.HashFromBytes(blob),
|
||||||
Content: blob,
|
Content: blob,
|
||||||
MimeType: mimeType,
|
MimeType: mediaType,
|
||||||
}
|
}
|
||||||
|
|
||||||
return icon, nil
|
return icon, nil
|
||||||
|
|
|
@ -25,6 +25,34 @@ func TestParseImageDataURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseImageDataURLWithNoEncoding(t *testing.T) {
|
||||||
|
iconURL := `data:image/webp,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E`
|
||||||
|
icon, err := parseImageDataURL(iconURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(`We should be able to parse valid data URL: %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if icon.MimeType != "image/webp" {
|
||||||
|
t.Fatal(`Invalid mime type parsed`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(icon.Content) == "Hello, World!" {
|
||||||
|
t.Fatal(`Value should be URL-decoded`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if icon.Hash == "" {
|
||||||
|
t.Fatal(`Image hash should be computed`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseImageDataURLWithNoMediaTypeAndNoEncoding(t *testing.T) {
|
||||||
|
iconURL := `data:,Hello%2C%20World%21`
|
||||||
|
_, err := parseImageDataURL(iconURL)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`We should detect invalid mime type`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseInvalidImageDataURLWithBadMimeType(t *testing.T) {
|
func TestParseInvalidImageDataURLWithBadMimeType(t *testing.T) {
|
||||||
_, err := parseImageDataURL("data:text/plain;base64,blob")
|
_, err := parseImageDataURL("data:text/plain;base64,blob")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -39,7 +67,7 @@ func TestParseInvalidImageDataURLWithUnsupportedEncoding(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseInvalidImageDataURLWithInvalidEncodedData(t *testing.T) {
|
func TestParseInvalidImageDataURLWithNoData(t *testing.T) {
|
||||||
_, err := parseImageDataURL("data:image/png;base64,")
|
_, err := parseImageDataURL("data:image/png;base64,")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal(`We should detect invalid encoded data`)
|
t.Fatal(`We should detect invalid encoded data`)
|
||||||
|
@ -53,6 +81,13 @@ func TestParseInvalidImageDataURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseInvalidImageDataURLWithWrongPrefix(t *testing.T) {
|
||||||
|
_, err := parseImageDataURL("data,test")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`We should detect malformed image data URL`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseDocumentWithWhitespaceIconURL(t *testing.T) {
|
func TestParseDocumentWithWhitespaceIconURL(t *testing.T) {
|
||||||
html := `<link rel="shortcut icon" href="
|
html := `<link rel="shortcut icon" href="
|
||||||
/static/img/favicon.ico
|
/static/img/favicon.ico
|
||||||
|
|
Loading…
Reference in a new issue