Ctrl K
ComunidadGuías…

pkg/jsplugin/icon.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package jsplugin

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"strings"
	"unicode"

	"github.com/MoeForge/moego/base64"
)

// MaxIconDataURIBytes bounds an uploaded plugin logo. It is deliberately
// generous: the plugin source itself is capped at 1 MiB, so this only keeps a
// logo from dwarfing the plugin it decorates.
const MaxIconDataURIBytes = 512 * 1024

var pngSignature = []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}

// DecodeIconDataURI parses a plugin logo shipped as a sidecar icon.svg or
// icon.png file and re-encoded by the uploader as
// data:image/png;base64,... or data:image/svg+xml;base64,.... It returns the
// media type and the raw image bytes. The admin UI renders logos only through
// <img>, which already blocks scripts and external loads; the SVG checks here
// are defense in depth so a hostile payload is rejected at upload instead of
// being stored.
func DecodeIconDataURI(icon string) (string, []byte, error) {
	if len(icon) > MaxIconDataURIBytes {
		return "", nil, fmt.Errorf("plugin icon must not exceed %d bytes", MaxIconDataURIBytes)
	}
	mediaType, payload, ok := strings.Cut(strings.TrimPrefix(icon, "data:"), ";base64,")
	if !strings.HasPrefix(icon, "data:") || !ok || (mediaType != "image/png" && mediaType != "image/svg+xml") {
		return "", nil, fmt.Errorf("plugin icon must be data:image/png;base64,... or data:image/svg+xml;base64,...")
	}
	decoded, err := base64.StdEncoding.Strict().DecodeString(payload)
	if err != nil {
		return "", nil, fmt.Errorf("plugin icon payload is not valid base64")
	}
	if err := ValidateIconImage(mediaType, decoded); err != nil {
		return "", nil, err
	}
	return mediaType, decoded, nil
}

// ValidateIconImage checks raw PNG or SVG bytes for a plugin logo. PNG must
// carry the PNG signature. SVG must be well-formed XML rooted at svg without
// script or foreignObject elements, event-handler attributes, directives,
// javascript: values, or absolute http(s) references.
func ValidateIconImage(mediaType string, data []byte) error {
	if mediaType == "image/png" {
		if !bytes.HasPrefix(data, pngSignature) {
			return fmt.Errorf("plugin icon PNG payload is not a PNG image")
		}
		return nil
	}
	if mediaType != "image/svg+xml" {
		return fmt.Errorf("plugin icon must be image/png or image/svg+xml")
	}
	decoder := xml.NewDecoder(bytes.NewReader(data))
	rootSeen := false
	styleDepth := 0
	for {
		token, err := decoder.Token()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("plugin icon SVG is not well-formed XML")
		}
		switch element := token.(type) {
		case xml.Directive:
			return fmt.Errorf("plugin icon SVG must not contain a DOCTYPE or other directives")
		case xml.ProcInst:
			if element.Target != "xml" {
				return fmt.Errorf("plugin icon SVG must not contain processing instructions")
			}
		case xml.StartElement:
			local := strings.ToLower(element.Name.Local)
			if !rootSeen {
				if local != "svg" {
					return fmt.Errorf("plugin icon SVG root element must be svg")
				}
				rootSeen = true
			}
			if local == "script" || local == "foreignobject" {
				return fmt.Errorf("plugin icon SVG must not contain %s elements", element.Name.Local)
			}
			if local == "style" {
				styleDepth++
			}
			for _, attr := range element.Attr {
				if attr.Name.Space == "xmlns" || (attr.Name.Space == "" && attr.Name.Local == "xmlns") {
					continue
				}
				if strings.HasPrefix(strings.ToLower(attr.Name.Local), "on") {
					return fmt.Errorf("plugin icon SVG must not contain event handler attributes")
				}
				if svgValueReferencesExternal(attr.Value) {
					return fmt.Errorf("plugin icon SVG must not reference scripts or external resources")
				}
			}
		case xml.EndElement:
			if strings.ToLower(element.Name.Local) == "style" && styleDepth > 0 {
				styleDepth--
			}
		case xml.CharData:
			if styleDepth > 0 && (svgValueReferencesExternal(string(element)) || strings.Contains(strings.ToLower(string(element)), "@import")) {
				return fmt.Errorf("plugin icon SVG must not reference scripts or external resources")
			}
		}
	}
	if !rootSeen {
		return fmt.Errorf("plugin icon SVG root element must be svg")
	}
	return nil
}

func svgValueReferencesExternal(value string) bool {
	compact := strings.Map(func(character rune) rune {
		if unicode.IsSpace(character) || unicode.IsControl(character) {
			return -1
		}
		return unicode.ToLower(character)
	}, value)
	return strings.Contains(compact, "javascript:") || strings.Contains(compact, "http://") || strings.Contains(compact, "https://")
}