package main
import (
"encoding/json"
"io/fs"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
func Base64Page(w http.ResponseWriter, r *http.Request) {
body := `
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(Page("/base64", "Base64 Scanner", body)))
}
func Base64PickFolder(w http.ResponseWriter, r *http.Request) {
p := PickFolder("Select folder to scan", "")
if p == "" {
writeJSON(w, map[string]any{"paths": []string{}})
return
}
writeJSON(w, map[string]any{"paths": collectHTMLFiles(p)})
}
func Base64PickFiles(w http.ResponseWriter, r *http.Request) {
paths := PickFiles(
"Select HTML files",
[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
true, "",
)
writeJSON(w, map[string]any{"paths": paths})
}
type scanResult struct {
File string `json:"file"`
OK bool `json:"ok"`
Assets []string `json:"assets"`
}
func Base64Scan(w http.ResponseWriter, r *http.Request) {
var req struct {
Folder string `json:"folder"`
Files []string `json:"files"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, map[string]any{"results": []scanResult{}, "error": err.Error()})
return
}
var targets []string
var baseDir string
if len(req.Files) > 0 {
for _, f := range req.Files {
if fileExists(f) {
targets = append(targets, f)
}
}
if len(targets) > 0 {
baseDir = commonBaseDir(targets)
}
} else if req.Folder != "" && fileExists(req.Folder) {
targets = collectHTMLFiles(req.Folder)
baseDir = req.Folder
} else {
writeJSON(w, map[string]any{"results": []scanResult{}, "error": "Pick a folder or files first"})
return
}
results := []scanResult{}
for _, file := range targets {
data, err := os.ReadFile(file)
if err != nil {
continue
}
offenders := findNonBase64Assets(string(data))
display := file
if baseDir != "" {
rel, err := filepath.Rel(baseDir, file)
if err == nil && rel != "" {
display = rel
} else {
display = filepath.Base(file)
}
}
results = append(results, scanResult{
File: display,
OK: len(offenders) == 0,
Assets: offenders,
})
}
writeJSON(w, map[string]any{"results": results})
}
func commonBaseDir(files []string) string {
if len(files) == 0 {
return ""
}
if len(files) == 1 {
return filepath.Dir(files[0])
}
split := make([][]string, len(files))
for i, f := range files {
split[i] = strings.Split(filepath.Dir(f), string(filepath.Separator))
}
first := split[0]
i := 0
for i < len(first) {
match := true
for _, parts := range split {
if i >= len(parts) || parts[i] != first[i] {
match = false
break
}
}
if !match {
break
}
i++
}
return strings.Join(first[:i], string(filepath.Separator))
}
func collectHTMLFiles(root string) []string {
var out []string
htmlRx := regexp.MustCompile(`(?i)\.html?$`)
_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
name := d.Name()
if name == "node_modules" || name == ".git" {
return filepath.SkipDir
}
return nil
}
if htmlRx.MatchString(d.Name()) {
out = append(out, path)
}
return nil
})
return out
}
var (
scriptBlockRx = regexp.MustCompile(`(?is)`)
attrRx = regexp.MustCompile(`(?i)\b(?:src|href|poster|data-src)\s*=\s*("([^"]*)"|'([^']*)')`)
cssUrlRx = regexp.MustCompile(`(?i)url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)`)
base64DataRx = regexp.MustCompile(`(?i)^data:[^;,]*;base64,`)
mailTelRx = regexp.MustCompile(`(?i)^(mailto:|tel:)`)
)
func findNonBase64Assets(htmlSrc string) []string {
markup := scriptBlockRx.ReplaceAllString(htmlSrc, "")
seen := map[string]bool{}
var out []string
add := func(u string) {
if u == "" || seen[u] {
return
}
seen[u] = true
out = append(out, u)
}
for _, m := range attrRx.FindAllStringSubmatch(markup, -1) {
url := m[2]
if url == "" {
url = m[3]
}
url = strings.TrimSpace(url)
if url == "" {
continue
}
if isAssetReference(url) && !base64DataRx.MatchString(url) {
add(url)
}
}
for _, m := range cssUrlRx.FindAllStringSubmatch(markup, -1) {
url := m[1]
if url == "" {
url = m[2]
}
if url == "" {
url = m[3]
}
url = strings.TrimSpace(url)
if url == "" {
continue
}
if !base64DataRx.MatchString(url) {
add(url)
}
}
if out == nil {
out = []string{}
}
return out
}
func isAssetReference(url string) bool {
if strings.HasPrefix(url, "#") || strings.HasPrefix(strings.ToLower(url), "javascript:") {
return false
}
if mailTelRx.MatchString(url) {
return false
}
return true
}