297 lines
7.3 KiB
Go
297 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"inou/lib"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Matches 16-char hex IDs (inou format) or standard UUIDs
|
|
var guidPattern = regexp.MustCompile(`[0-9a-fA-F]{16}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)
|
|
|
|
const tarpitDuration = 30 * time.Second
|
|
|
|
var tarpitPatterns = []string{
|
|
// PHP/CMS
|
|
"php", "wp-", "xmlrpc", "phpmyadmin", "pma", "adminer",
|
|
"joomla", "drupal", "magento", "administrator",
|
|
// Config/Secrets
|
|
".env", ".git", ".svn", ".htaccess", ".htpasswd",
|
|
".aws", ".docker", ".bak", ".sql",
|
|
// Java/Spring
|
|
"actuator", "jolokia", "console",
|
|
// ColdFusion
|
|
"cfide", "cf_scripts",
|
|
// Shells/Exploits
|
|
"shell", "cgi-bin", "elfinder", "filemanager", "fckeditor",
|
|
// Infrastructure
|
|
"/manager/", "jenkins", "jmx-console", "web-console",
|
|
"solr", "telescope", "elmah",
|
|
// Network devices
|
|
"goform", "hnap", "stssys",
|
|
}
|
|
|
|
var validPaths = []string{
|
|
"/",
|
|
"/static/",
|
|
"/robots.txt",
|
|
"/sitemap.xml",
|
|
"/favicon.ico",
|
|
"/apple-touch-icon.png",
|
|
"/apple-touch-icon-precomposed.png",
|
|
"/app-ads.txt",
|
|
"/llms.txt",
|
|
"/send-code",
|
|
"/verify",
|
|
"/onboard",
|
|
"/logout",
|
|
"/dashboard",
|
|
"/connect",
|
|
"/install",
|
|
"/start",
|
|
"/invite",
|
|
"/set-lang",
|
|
"/privacy-policy",
|
|
"/security",
|
|
"/legal/dpa",
|
|
"/legal/terms",
|
|
"/demo",
|
|
"/oauth/authorize",
|
|
"/oauth/token",
|
|
"/oauth/userinfo",
|
|
"/oauth/revoke",
|
|
"/.well-known/",
|
|
"/register",
|
|
"/styleguide",
|
|
"/pricing",
|
|
"/faq",
|
|
"/dossier/",
|
|
"/viewer/",
|
|
"/data/",
|
|
"/image/",
|
|
"/contact-sheet.webp/",
|
|
"/download/",
|
|
// API - specific routes only (no broad /api prefix)
|
|
"/api/docs",
|
|
"/api/openapi.yaml",
|
|
"/api/token/",
|
|
"/api/version",
|
|
"/api/dossier", // covers /api/dossier and /api/dossiers
|
|
"/api/studies",
|
|
"/api/series",
|
|
"/api/slices",
|
|
"/api/labs/",
|
|
"/api/access",
|
|
"/api/audit",
|
|
"/api/entries",
|
|
"/api/trackers",
|
|
"/api/v1/auth/",
|
|
"/api/v1/dashboard",
|
|
"/api/v1/trackers",
|
|
"/api/v1/health",
|
|
"/api/v1/token",
|
|
"/api/v1/dossiers",
|
|
"/api/v1/images/",
|
|
"/api/v1/categories",
|
|
}
|
|
|
|
var whitelistedIPs = map[string]bool{
|
|
"82.22.36.202": true, // our vulnerability scanner
|
|
}
|
|
|
|
func isLocalIP(ip string) bool {
|
|
return strings.HasPrefix(ip, "192.168.")
|
|
}
|
|
|
|
func getIP(r *http.Request) string {
|
|
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
|
return strings.Split(fwd, ",")[0]
|
|
}
|
|
return strings.Split(r.RemoteAddr, ":")[0]
|
|
}
|
|
|
|
func isValidPath(path string) bool {
|
|
if path == "/api" || path == "/mcp" {
|
|
return true
|
|
}
|
|
for _, p := range validPaths {
|
|
if p == "/" && path == "/" {
|
|
return true
|
|
}
|
|
if p != "/" && strings.HasPrefix(path, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func shouldTarpit(path string) bool {
|
|
lower := strings.ToLower(path)
|
|
if strings.HasSuffix(lower, ".js") &&
|
|
lower != "/static/upload.js" &&
|
|
lower != "/static/carousel.js" &&
|
|
lower != "/static/viewer.js" {
|
|
return true
|
|
}
|
|
for _, pattern := range tarpitPatterns {
|
|
if strings.Contains(lower, pattern) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasGUID checks if path or query contains a GUID-like token
|
|
// This indicates a legitimate client with invalid/expired credentials
|
|
func hasGUID(r *http.Request) bool {
|
|
// Check path
|
|
if guidPattern.MatchString(r.URL.Path) {
|
|
return true
|
|
}
|
|
// Check query params (token=xxx, dossier=xxx, etc.)
|
|
if guidPattern.MatchString(r.URL.RawQuery) {
|
|
return true
|
|
}
|
|
// Check Authorization header
|
|
if auth := r.Header.Get("Authorization"); auth != "" {
|
|
if guidPattern.MatchString(auth) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// tarpit holds connection silent then closes - no HTTP response
|
|
func tarpit(w http.ResponseWriter, r *http.Request, label string) {
|
|
ip := getIP(r)
|
|
logAccess("\033[33m%s %s %s %s %s\033[0m",
|
|
time.Now().Format("2006-01-02 15:04:05"),
|
|
ip, label, r.Method, r.URL.RequestURI())
|
|
|
|
w.WriteHeader(444)
|
|
}
|
|
|
|
func defense(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
ip := getIP(r)
|
|
|
|
// Known bad patterns - tarpit immediately (unless GUID present)
|
|
if shouldTarpit(strings.ToLower(path)) {
|
|
if hasGUID(r) {
|
|
// Looks like a real client with bad/expired token - give proper error
|
|
logAccess("\033[33m%s %s INVALID_TOKEN %s %s\033[0m",
|
|
time.Now().Format("2006-01-02 15:04:05"),
|
|
ip, r.Method, r.URL.RequestURI())
|
|
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
tarpit(w, r, "TARPIT")
|
|
return
|
|
}
|
|
|
|
// MCP without Bearer token - not a legitimate client
|
|
if path == "/mcp" && !isLocalIP(ip) {
|
|
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
|
tarpit(w, r, "MCP_NO_AUTH")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Unknown paths from non-local IPs
|
|
if !isValidPath(path) && !isLocalIP(ip) {
|
|
if hasGUID(r) {
|
|
// Looks like a real client with bad/expired token - give proper error
|
|
logAccess("\033[33m%s %s INVALID_TOKEN %s %s\033[0m",
|
|
time.Now().Format("2006-01-02 15:04:05"),
|
|
ip, r.Method, r.URL.RequestURI())
|
|
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
tarpit(w, r, "PROBE")
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
type statusCapture struct {
|
|
http.ResponseWriter
|
|
status int
|
|
r *http.Request
|
|
}
|
|
|
|
func (s *statusCapture) WriteHeader(code int) {
|
|
s.status = code
|
|
if code == 404 && s.r.URL.Path != "/favicon.ico" {
|
|
ip := getIP(s.r)
|
|
if !whitelistedIPs[ip] {
|
|
lib.SendSignal(fmt.Sprintf("404: %s %s", ip, s.r.URL.Path))
|
|
}
|
|
}
|
|
s.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func notifyOn404(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
capture := &statusCapture{ResponseWriter: w, status: 200, r: r}
|
|
next.ServeHTTP(capture, r)
|
|
})
|
|
}
|
|
|
|
func normalizeQuery(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
raw := r.URL.RawQuery
|
|
raw = strings.ReplaceAll(raw, "&%253B", "&")
|
|
raw = strings.ReplaceAll(raw, "&%3B", "&")
|
|
raw = strings.ReplaceAll(raw, "&", "&")
|
|
raw = strings.ReplaceAll(raw, "%26amp%3B", "&")
|
|
raw = strings.ReplaceAll(raw, "%26amp;", "&")
|
|
r.URL.RawQuery = raw
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
// httpsRedirectMiddleware redirects HTTP requests to HTTPS
|
|
// Respects X-Forwarded-Proto header for deployments behind reverse proxy
|
|
func httpsRedirectMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Skip redirect for health checks and local development
|
|
if r.URL.Path == "/api/v1/health" || isLocalIP(getIP(r)) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Check if request is already HTTPS (direct or via proxy)
|
|
isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
|
|
|
|
if !isHTTPS {
|
|
// Build HTTPS URL
|
|
target := "https://" + r.Host + r.URL.RequestURI()
|
|
http.Redirect(w, r, target, http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// hstsMiddleware adds HTTP Strict Transport Security headers
|
|
func hstsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Only add HSTS header for HTTPS requests
|
|
isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
|
|
|
|
if isHTTPS {
|
|
// max-age=1 year (31536000 seconds), include subdomains, preload ready
|
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|