header analyzer fix large headers
This commit is contained in:
7550
example.txt
Normal file
7550
example.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
headeranalyzer
BIN
headeranalyzer
Binary file not shown.
@@ -3,12 +3,19 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"headeranalyzer/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
|
csrf *security.CSRFManager
|
||||||
|
validator *security.InputValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(embeddedFiles embed.FS) *Handler {
|
func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||||
@@ -56,13 +63,38 @@ func NewHandler(embeddedFiles embed.FS) *Handler {
|
|||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
templates: tmpl,
|
templates: tmpl,
|
||||||
|
csrf: security.NewCSRFManager(time.Hour),
|
||||||
|
validator: security.NewInputValidator(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
|
// Validate CSRF token
|
||||||
|
csrfToken := r.FormValue("csrf_token")
|
||||||
|
if !h.csrf.ValidateToken(csrfToken) {
|
||||||
|
http.Redirect(w, r, "/?error="+url.QueryEscape("Invalid security token. Please try again."), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate headers input
|
||||||
headers := r.FormValue("headers")
|
headers := r.FormValue("headers")
|
||||||
report := Analyze(headers)
|
log.Printf("DEBUG: Received headers input: %d characters", len(headers))
|
||||||
|
|
||||||
|
validatedHeaders, err := h.validator.ValidateEmailHeaders(headers)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("DEBUG: Validation failed: %v", err)
|
||||||
|
if security.IsValidationError(err) {
|
||||||
|
http.Redirect(w, r, "/?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/?error="+url.QueryEscape("Invalid input provided"), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("DEBUG: Headers validated successfully")
|
||||||
|
report := Analyze(validatedHeaders)
|
||||||
|
log.Printf("DEBUG: Analysis completed, From field: '%s'", report.From)
|
||||||
// Create a wrapper struct to include current page info
|
// Create a wrapper struct to include current page info
|
||||||
data := struct {
|
data := struct {
|
||||||
*Report
|
*Report
|
||||||
@@ -71,16 +103,34 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
Report: report,
|
Report: report,
|
||||||
CurrentPage: "home",
|
CurrentPage: "home",
|
||||||
}
|
}
|
||||||
h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
|
|
||||||
|
log.Printf("DEBUG: About to render template with data")
|
||||||
|
err = h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: Template execution failed: %v", err)
|
||||||
|
http.Error(w, "Template rendering failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("DEBUG: Template rendered successfully")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate CSRF token for GET requests
|
||||||
|
csrfToken, err := h.csrf.GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// For GET requests, create an empty report so template conditions work
|
// For GET requests, create an empty report so template conditions work
|
||||||
data := struct {
|
data := struct {
|
||||||
*Report
|
*Report
|
||||||
CurrentPage string
|
CurrentPage string
|
||||||
|
CSRFToken string
|
||||||
}{
|
}{
|
||||||
Report: &Report{}, // Empty report so .From will be empty string
|
Report: &Report{}, // Empty report so .From will be empty string
|
||||||
CurrentPage: "home",
|
CurrentPage: "home",
|
||||||
|
CSRFToken: csrfToken,
|
||||||
}
|
}
|
||||||
h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
|
h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package passwordgenerator
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"headeranalyzer/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var validator = security.NewInputValidator()
|
||||||
|
|
||||||
// PasswordAPIHandler handles password generation requests
|
// PasswordAPIHandler handles password generation requests
|
||||||
func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
@@ -32,6 +36,45 @@ func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate input parameters
|
||||||
|
if requestData.Length < 4 || requestData.Length > 128 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Length must be between 4 and 128"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestData.NumberCount < 0 || requestData.NumberCount > 20 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Number count must be between 0 and 20"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestData.WordCount < 2 || requestData.WordCount > 10 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Word count must be between 2 and 10"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(requestData.SpecialChars) > 50 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Special characters string too long"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate type parameter
|
||||||
|
if requestData.Type != "random" && requestData.Type != "passphrase" {
|
||||||
|
requestData.Type = "passphrase" // Default to passphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate number position
|
||||||
|
validPositions := map[string]bool{"start": true, "end": true, "each": true}
|
||||||
|
if !validPositions[requestData.NumberPosition] {
|
||||||
|
requestData.NumberPosition = "end" // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize special characters to prevent potential issues
|
||||||
|
requestData.SpecialChars = validator.SanitizeHTML(requestData.SpecialChars)
|
||||||
|
|
||||||
// Convert to internal Config format
|
// Convert to internal Config format
|
||||||
config := Config{
|
config := Config{
|
||||||
Length: requestData.Length,
|
Length: requestData.Length,
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"headeranalyzer/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
|
csrf *security.CSRFManager
|
||||||
|
validator *security.InputValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasswordConfig struct {
|
type PasswordConfig struct {
|
||||||
@@ -57,10 +63,19 @@ func NewHandler(embeddedFiles embed.FS) *Handler {
|
|||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
templates: tmpl,
|
templates: tmpl,
|
||||||
|
csrf: security.NewCSRFManager(time.Hour),
|
||||||
|
validator: security.NewInputValidator(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Generate CSRF token
|
||||||
|
csrfToken, err := h.csrf.GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/password?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Parse URL parameters to set default values
|
// Parse URL parameters to set default values
|
||||||
config := PasswordConfig{
|
config := PasswordConfig{
|
||||||
Type: getStringParam(r, "type", "passphrase"),
|
Type: getStringParam(r, "type", "passphrase"),
|
||||||
@@ -80,9 +95,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
data := struct {
|
data := struct {
|
||||||
CurrentPage string
|
CurrentPage string
|
||||||
Config PasswordConfig
|
Config PasswordConfig
|
||||||
|
CSRFToken string
|
||||||
}{
|
}{
|
||||||
CurrentPage: "password",
|
CurrentPage: "password",
|
||||||
Config: config,
|
Config: config,
|
||||||
|
CSRFToken: csrfToken,
|
||||||
}
|
}
|
||||||
h.templates.ExecuteTemplate(w, "password.html", data)
|
h.templates.ExecuteTemplate(w, "password.html", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -17,11 +16,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
"headeranalyzer/security"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -33,7 +32,8 @@ type PWPusher struct {
|
|||||||
pushTemplates *template.Template
|
pushTemplates *template.Template
|
||||||
viewTemplates *template.Template
|
viewTemplates *template.Template
|
||||||
failedAttempts map[string]*FailedAttempts
|
failedAttempts map[string]*FailedAttempts
|
||||||
csrfTokens map[string]time.Time // Map of CSRF tokens to their creation time
|
csrf *security.CSRFManager
|
||||||
|
validator *security.InputValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
type FailedAttempts struct {
|
type FailedAttempts struct {
|
||||||
@@ -115,12 +115,6 @@ const (
|
|||||||
CSRFTokenExpiry = 1 * time.Hour // CSRF tokens expire after 1 hour
|
CSRFTokenExpiry = 1 * time.Hour // CSRF tokens expire after 1 hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// Input validation patterns
|
|
||||||
var (
|
|
||||||
allowedActionPattern = regexp.MustCompile(`^(reveal|verify_password|delete)$`)
|
|
||||||
safeStringPattern = regexp.MustCompile(`^[\p{L}\p{N}\p{P}\p{Z}\p{S}\s]*$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewPWPusher creates a new PWPusher instance
|
// NewPWPusher creates a new PWPusher instance
|
||||||
func NewPWPusher(embeddedFS fs.FS, encryptionKey string) (*PWPusher, error) {
|
func NewPWPusher(embeddedFS fs.FS, encryptionKey string) (*PWPusher, error) {
|
||||||
if encryptionKey == "" {
|
if encryptionKey == "" {
|
||||||
@@ -156,7 +150,8 @@ func NewPWPusher(embeddedFS fs.FS, encryptionKey string) (*PWPusher, error) {
|
|||||||
pushTemplates: pushTemplates,
|
pushTemplates: pushTemplates,
|
||||||
viewTemplates: viewTemplates,
|
viewTemplates: viewTemplates,
|
||||||
failedAttempts: make(map[string]*FailedAttempts),
|
failedAttempts: make(map[string]*FailedAttempts),
|
||||||
csrfTokens: make(map[string]time.Time),
|
csrf: security.NewCSRFManager(CSRFTokenExpiry),
|
||||||
|
validator: security.NewInputValidator(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,29 +465,7 @@ func (p *PWPusher) getBlockedUntil(clientIP string) time.Time {
|
|||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PWPusher) getClientIP(r *http.Request) string {
|
// HTTP Handlers
|
||||||
// Check X-Forwarded-For header for real IP behind proxy
|
|
||||||
forwarded := r.Header.Get("X-Forwarded-For")
|
|
||||||
if forwarded != "" {
|
|
||||||
// Get the first IP in the list
|
|
||||||
ips := strings.Split(forwarded, ",")
|
|
||||||
clientIP := strings.TrimSpace(ips[0])
|
|
||||||
log.Printf("Using X-Forwarded-For IP: %s", clientIP)
|
|
||||||
return clientIP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check X-Real-IP header
|
|
||||||
realIP := r.Header.Get("X-Real-IP")
|
|
||||||
if realIP != "" {
|
|
||||||
log.Printf("Using X-Real-IP: %s", realIP)
|
|
||||||
return realIP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to remote address
|
|
||||||
clientIP := strings.Split(r.RemoteAddr, ":")[0]
|
|
||||||
log.Printf("Using RemoteAddr IP: %s", clientIP)
|
|
||||||
return clientIP
|
|
||||||
} // HTTP Handlers
|
|
||||||
|
|
||||||
func (p *PWPusher) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
func (p *PWPusher) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("PWPusher IndexHandler called: %s %s", r.Method, r.URL.Path)
|
log.Printf("PWPusher IndexHandler called: %s %s", r.Method, r.URL.Path)
|
||||||
@@ -574,24 +547,24 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Comprehensive input validation
|
// Comprehensive input validation
|
||||||
sanitizedText, err := p.validateAndSanitizeText(req.Text)
|
sanitizedText, err := p.validator.ValidateAndSanitizeText(req.Text, 100000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Text = sanitizedText
|
req.Text = sanitizedText
|
||||||
|
|
||||||
if err := p.validatePassword(req.Password); err != nil {
|
if err := p.validator.ValidatePassword(req.Password); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.validateInteger(req.ExpiryDays, MinExpiryDays, MaxExpiryDays, "expiry days"); err != nil {
|
if err := p.validator.ValidateIntRange(req.ExpiryDays, MinExpiryDays, MaxExpiryDays, "expiry days"); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.validateInteger(req.MaxViews, MinViews, MaxViews, "max views"); err != nil {
|
if err := p.validator.ValidateIntRange(req.MaxViews, MinViews, MaxViews, "max views"); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -611,7 +584,7 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
|||||||
id := p.generateID()
|
id := p.generateID()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expiresAt := now.AddDate(0, 0, req.ExpiryDays)
|
expiresAt := now.AddDate(0, 0, req.ExpiryDays)
|
||||||
creatorIP := p.getClientIP(r)
|
creatorIP := p.validator.GetClientIP(r)
|
||||||
|
|
||||||
// Hash password if provided
|
// Hash password if provided
|
||||||
var passwordHash sql.NullString
|
var passwordHash sql.NullString
|
||||||
@@ -794,7 +767,7 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
} else if action == "verify_password" {
|
} else if action == "verify_password" {
|
||||||
// Handle password verification with rate limiting
|
// Handle password verification with rate limiting
|
||||||
clientIP := p.getClientIP(r)
|
clientIP := p.validator.GetClientIP(r)
|
||||||
log.Printf("Password verification attempt from IP: %s", clientIP)
|
log.Printf("Password verification attempt from IP: %s", clientIP)
|
||||||
|
|
||||||
// Check if client is blocked
|
// Check if client is blocked
|
||||||
@@ -937,7 +910,7 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Check if password is required and not yet verified
|
// Check if password is required and not yet verified
|
||||||
if push.PasswordHash != "" {
|
if push.PasswordHash != "" {
|
||||||
clientIP := p.getClientIP(r)
|
clientIP := p.validator.GetClientIP(r)
|
||||||
|
|
||||||
// Check if client is blocked
|
// Check if client is blocked
|
||||||
if p.isBlocked(clientIP) {
|
if p.isBlocked(clientIP) {
|
||||||
@@ -1089,112 +1062,13 @@ func (p *PWPusher) StatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input validation and sanitization functions
|
|
||||||
func (p *PWPusher) validateAndSanitizeText(text string) (string, error) {
|
|
||||||
if text == "" {
|
|
||||||
return "", fmt.Errorf("text cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(text) > MaxTextLength {
|
|
||||||
return "", fmt.Errorf("text too long (max %d characters)", MaxTextLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utf8.ValidString(text) {
|
|
||||||
return "", fmt.Errorf("text contains invalid UTF-8 characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the original text without HTML escaping
|
|
||||||
// The template will handle safe display
|
|
||||||
return text, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PWPusher) validatePassword(password string) error {
|
|
||||||
if len(password) > MaxPasswordLength {
|
|
||||||
return fmt.Errorf("password too long (max %d characters)", MaxPasswordLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(password) > 0 && len(password) < MinPasswordLength {
|
|
||||||
return fmt.Errorf("password too short (min %d characters)", MinPasswordLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utf8.ValidString(password) {
|
|
||||||
return fmt.Errorf("password contains invalid UTF-8 characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PWPusher) validateInteger(value, min, max int, fieldName string) error {
|
|
||||||
if value < min || value > max {
|
|
||||||
return fmt.Errorf("%s must be between %d and %d", fieldName, min, max)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PWPusher) validateAction(action string) error {
|
|
||||||
if !allowedActionPattern.MatchString(action) {
|
|
||||||
return fmt.Errorf("invalid action")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PWPusher) sanitizeString(input string) string {
|
|
||||||
// Remove any potentially dangerous characters and HTML escape
|
|
||||||
sanitized := html.EscapeString(strings.TrimSpace(input))
|
|
||||||
|
|
||||||
// Limit length
|
|
||||||
if len(sanitized) > 1000 {
|
|
||||||
sanitized = sanitized[:1000]
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitized
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSRF token management
|
// CSRF token management
|
||||||
func (p *PWPusher) generateCSRFToken() (string, error) {
|
func (p *PWPusher) generateCSRFToken() (string, error) {
|
||||||
bytes := make([]byte, 32)
|
return p.csrf.GenerateToken()
|
||||||
_, err := rand.Read(bytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
token := base64.URLEncoding.EncodeToString(bytes)
|
|
||||||
p.csrfTokens[token] = time.Now()
|
|
||||||
|
|
||||||
// Clean up expired tokens
|
|
||||||
p.cleanupExpiredCSRFTokens()
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PWPusher) validateCSRFToken(token string) bool {
|
func (p *PWPusher) validateCSRFToken(token string) bool {
|
||||||
if token == "" {
|
return p.csrf.ValidateToken(token)
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAt, exists := p.csrfTokens[token]
|
|
||||||
if !exists {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token has expired
|
|
||||||
if time.Since(createdAt) > CSRFTokenExpiry {
|
|
||||||
delete(p.csrfTokens, token)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove token after use (one-time use)
|
|
||||||
delete(p.csrfTokens, token)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PWPusher) cleanupExpiredCSRFTokens() {
|
|
||||||
now := time.Now()
|
|
||||||
for token, createdAt := range p.csrfTokens {
|
|
||||||
if now.Sub(createdAt) > CSRFTokenExpiry {
|
|
||||||
delete(p.csrfTokens, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CSRF token to ViewData
|
// Add CSRF token to ViewData
|
||||||
@@ -1313,16 +1187,6 @@ func (p *PWPusher) setHistoryCookies(w http.ResponseWriter, history []map[string
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PWPusher) clearHistoryCookies(w http.ResponseWriter) {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "pwpush_history",
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
MaxAge: -1,
|
|
||||||
HttpOnly: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PWPusher) renderTemplate(w http.ResponseWriter, templateName string, data interface{}) {
|
func (p *PWPusher) renderTemplate(w http.ResponseWriter, templateName string, data interface{}) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -7,32 +7,68 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"headeranalyzer/security"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var validator = security.NewInputValidator()
|
||||||
|
|
||||||
// DNSAPIHandler handles DNS lookup requests
|
// DNSAPIHandler handles DNS lookup requests
|
||||||
func DNSAPIHandler(w http.ResponseWriter, r *http.Request) {
|
func DNSAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Validate and sanitize query parameter
|
||||||
query := r.URL.Query().Get("query")
|
query := r.URL.Query().Get("query")
|
||||||
typeq := r.URL.Query().Get("type")
|
validatedQuery, err := validator.ValidateDNSQuery(query)
|
||||||
if query == "" || typeq == "" {
|
if err != nil {
|
||||||
w.WriteHeader(400)
|
w.WriteHeader(400)
|
||||||
w.Write([]byte("Missing query or type"))
|
w.Write([]byte("Invalid query: " + err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate type parameter
|
||||||
|
typeq := r.URL.Query().Get("type")
|
||||||
|
if typeq == "" {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
w.Write([]byte("Missing type parameter"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate allowed DNS types
|
||||||
|
allowedTypes := map[string]bool{
|
||||||
|
"A": true, "AAAA": true, "MX": true, "TXT": true, "NS": true,
|
||||||
|
"CNAME": true, "PTR": true, "SOA": true, "SPF": true,
|
||||||
|
"DKIM": true, "DMARC": true, "WHOIS": true,
|
||||||
|
}
|
||||||
|
if !allowedTypes[typeq] {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
w.Write([]byte("Invalid DNS type"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize server parameter if provided
|
||||||
dnsServer := r.URL.Query().Get("server")
|
dnsServer := r.URL.Query().Get("server")
|
||||||
|
if dnsServer != "" {
|
||||||
|
// Basic validation for DNS server format
|
||||||
|
if len(dnsServer) > 100 || strings.ContainsAny(dnsServer, "\r\n\x00") {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
w.Write([]byte("Invalid DNS server"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var result string
|
var result string
|
||||||
|
|
||||||
switch typeq {
|
switch typeq {
|
||||||
case "WHOIS":
|
case "WHOIS":
|
||||||
result = handleWHOISQuery(query)
|
result = handleWHOISQuery(validatedQuery)
|
||||||
case "SPF":
|
case "SPF":
|
||||||
result = handleSPFQuery(query, dnsServer)
|
result = handleSPFQuery(validatedQuery, dnsServer)
|
||||||
case "DMARC":
|
case "DMARC":
|
||||||
result = handleDMARCQuery(query, dnsServer)
|
result = handleDMARCQuery(validatedQuery, dnsServer)
|
||||||
case "DKIM":
|
case "DKIM":
|
||||||
result = handleDKIMQuery(query, dnsServer, r)
|
result = handleDKIMQuery(validatedQuery, dnsServer, r)
|
||||||
default:
|
default:
|
||||||
result = handleStandardDNSQuery(query, typeq, dnsServer)
|
result = handleStandardDNSQuery(validatedQuery, typeq, dnsServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"headeranalyzer/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
|
csrf *security.CSRFManager
|
||||||
|
validator *security.InputValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(embeddedFiles embed.FS) *Handler {
|
func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||||
@@ -41,14 +47,25 @@ func NewHandler(embeddedFiles embed.FS) *Handler {
|
|||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
templates: tmpl,
|
templates: tmpl,
|
||||||
|
csrf: security.NewCSRFManager(time.Hour),
|
||||||
|
validator: security.NewInputValidator(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Generate CSRF token for the page
|
||||||
|
csrfToken, err := h.csrf.GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/dns?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
CurrentPage string
|
CurrentPage string
|
||||||
|
CSRFToken string
|
||||||
}{
|
}{
|
||||||
CurrentPage: "dns",
|
CurrentPage: "dns",
|
||||||
|
CSRFToken: csrfToken,
|
||||||
}
|
}
|
||||||
h.templates.ExecuteTemplate(w, "dns.html", data)
|
h.templates.ExecuteTemplate(w, "dns.html", data)
|
||||||
}
|
}
|
||||||
|
|||||||
84
security/csrf.go
Normal file
84
security/csrf.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CSRFManager handles CSRF token generation and validation
|
||||||
|
type CSRFManager struct {
|
||||||
|
tokens map[string]time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
expiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCSRFManager creates a new CSRF manager with the specified token expiry duration
|
||||||
|
func NewCSRFManager(expiry time.Duration) *CSRFManager {
|
||||||
|
return &CSRFManager{
|
||||||
|
tokens: make(map[string]time.Time),
|
||||||
|
expiry: expiry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken creates a new CSRF token
|
||||||
|
func (c *CSRFManager) GenerateToken() (string, error) {
|
||||||
|
bytes := make([]byte, 32)
|
||||||
|
_, err := rand.Read(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := base64.URLEncoding.EncodeToString(bytes)
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.tokens[token] = time.Now()
|
||||||
|
c.cleanupExpiredTokensUnsafe()
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken validates a CSRF token and removes it (one-time use)
|
||||||
|
func (c *CSRFManager) ValidateToken(token string) bool {
|
||||||
|
if token == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
createdAt, exists := c.tokens[token]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token has expired
|
||||||
|
if time.Since(createdAt) > c.expiry {
|
||||||
|
delete(c.tokens, token)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove token after use (one-time use)
|
||||||
|
delete(c.tokens, token)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupExpiredTokensUnsafe removes expired tokens (must be called with mutex locked)
|
||||||
|
func (c *CSRFManager) cleanupExpiredTokensUnsafe() {
|
||||||
|
now := time.Now()
|
||||||
|
for token, createdAt := range c.tokens {
|
||||||
|
if now.Sub(createdAt) > c.expiry {
|
||||||
|
delete(c.tokens, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupExpiredTokens removes expired tokens (thread-safe)
|
||||||
|
func (c *CSRFManager) CleanupExpiredTokens() {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
c.cleanupExpiredTokensUnsafe()
|
||||||
|
}
|
||||||
336
security/validation.go
Normal file
336
security/validation.go
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputValidator provides input validation and sanitization methods
|
||||||
|
type InputValidator struct{}
|
||||||
|
|
||||||
|
// NewInputValidator creates a new input validator
|
||||||
|
func NewInputValidator() *InputValidator {
|
||||||
|
return &InputValidator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeHTML escapes HTML characters to prevent XSS
|
||||||
|
func (v *InputValidator) SanitizeHTML(input string) string {
|
||||||
|
return html.EscapeString(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSanitizeText validates text input and removes dangerous content
|
||||||
|
func (v *InputValidator) ValidateAndSanitizeText(text string, maxLength int) (string, error) {
|
||||||
|
// Check for empty input
|
||||||
|
if strings.TrimSpace(text) == "" {
|
||||||
|
return "", &ValidationError{Field: "text", Message: "Text cannot be empty"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check length limits
|
||||||
|
if len(text) > maxLength {
|
||||||
|
return "", &ValidationError{Field: "text", Message: "Text exceeds maximum length"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid UTF-8
|
||||||
|
if !utf8.ValidString(text) {
|
||||||
|
return "", &ValidationError{Field: "text", Message: "Text contains invalid characters"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove null bytes and control characters (except normal whitespace)
|
||||||
|
cleaned := regexp.MustCompile(`[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]`).ReplaceAllString(text, "")
|
||||||
|
|
||||||
|
// Normalize line endings
|
||||||
|
cleaned = strings.ReplaceAll(cleaned, "\r\n", "\n")
|
||||||
|
cleaned = strings.ReplaceAll(cleaned, "\r", "\n")
|
||||||
|
|
||||||
|
return cleaned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePassword validates password requirements
|
||||||
|
func (v *InputValidator) ValidatePassword(password string) error {
|
||||||
|
if len(password) < 1 {
|
||||||
|
return &ValidationError{Field: "password", Message: "Password cannot be empty"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(password) > 1000 {
|
||||||
|
return &ValidationError{Field: "password", Message: "Password too long"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid UTF-8
|
||||||
|
if !utf8.ValidString(password) {
|
||||||
|
return &ValidationError{Field: "password", Message: "Password contains invalid characters"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateEmailHeaders validates email header input with enhanced detection and large file handling
|
||||||
|
func (v *InputValidator) ValidateEmailHeaders(headers string) (string, error) {
|
||||||
|
if strings.TrimSpace(headers) == "" {
|
||||||
|
return "", &ValidationError{Field: "headers", Message: "Email headers cannot be empty"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid UTF-8
|
||||||
|
if !utf8.ValidString(headers) {
|
||||||
|
return "", &ValidationError{Field: "headers", Message: "Headers contain invalid characters"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract only the header portion for large files
|
||||||
|
processedHeaders := v.extractEmailHeadersOnly(headers)
|
||||||
|
|
||||||
|
// Check if we have valid email headers
|
||||||
|
if !v.containsValidEmailHeaders(processedHeaders) {
|
||||||
|
return "", &ValidationError{Field: "headers", Message: "No valid email headers found. Please provide actual email headers."}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After processing, check reasonable size limit
|
||||||
|
if len(processedHeaders) > 200*1024 { // 200KB after processing
|
||||||
|
return "", &ValidationError{Field: "headers", Message: "Email headers too large after processing"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractEmailHeadersOnly extracts only the email headers from potentially large files
|
||||||
|
func (v *InputValidator) extractEmailHeadersOnly(content string) string {
|
||||||
|
var result strings.Builder
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
headerSection := true
|
||||||
|
headerLines := 0
|
||||||
|
maxHeaderLines := 1000
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimRight(line, "\r")
|
||||||
|
|
||||||
|
// Stop if we've processed too many header lines (safety limit)
|
||||||
|
if headerLines >= maxHeaderLines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty line typically separates headers from body
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
if headerSection && headerLines > 0 {
|
||||||
|
// End of headers section
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME boundary indicates end of headers/start of body content
|
||||||
|
if strings.HasPrefix(line, "--") && strings.Contains(line, "boundary") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip obvious encoded content (base64-like long strings without spaces)
|
||||||
|
if len(line) > 200 && !strings.Contains(line, " ") && !strings.Contains(line, ":") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip lines that look like encoded content
|
||||||
|
if v.looksLikeEncodedContent(line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this looks like a header line
|
||||||
|
if headerSection && (v.looksLikeEmailHeader(line) || v.isHeaderContinuation(line)) {
|
||||||
|
result.WriteString(line)
|
||||||
|
result.WriteString("\n")
|
||||||
|
headerLines++
|
||||||
|
} else if headerSection && !v.looksLikeEmailHeader(line) && !v.isHeaderContinuation(line) {
|
||||||
|
// If we encounter a non-header line in header section, we might be done with headers
|
||||||
|
if headerLines > 5 { // Only stop if we've seen some headers already
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsValidEmailHeaders checks if the content contains actual email headers
|
||||||
|
func (v *InputValidator) containsValidEmailHeaders(content string) bool {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
headerCount := 0
|
||||||
|
commonHeaders := []string{
|
||||||
|
"received:", "from:", "to:", "subject:", "date:", "message-id:",
|
||||||
|
"return-path:", "delivered-to:", "authentication-results:",
|
||||||
|
"dkim-signature:", "content-type:", "mime-version:", "x-",
|
||||||
|
"reply-to:", "cc:", "bcc:", "sender:", "list-id:",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.ToLower(strings.TrimSpace(line))
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this line contains a common email header
|
||||||
|
for _, header := range commonHeaders {
|
||||||
|
if strings.HasPrefix(line, header) {
|
||||||
|
headerCount++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for basic header format
|
||||||
|
if strings.Contains(line, ":") && !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
|
||||||
|
// Additional validation for header format
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
headerName := strings.TrimSpace(parts[0])
|
||||||
|
// Header name should contain only valid characters
|
||||||
|
if regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString(headerName) {
|
||||||
|
headerCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 3 valid headers to consider it email headers
|
||||||
|
return headerCount >= 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// looksLikeEmailHeader checks if a line looks like an email header
|
||||||
|
func (v *InputValidator) looksLikeEmailHeader(line string) bool {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header continuation lines start with space or tab
|
||||||
|
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||||
|
return false // These are handled separately
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must contain a colon
|
||||||
|
if !strings.Contains(line, ":") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on first colon
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
headerName := strings.TrimSpace(parts[0])
|
||||||
|
|
||||||
|
// Header name should not be empty and should contain only valid characters
|
||||||
|
if headerName == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid header name pattern
|
||||||
|
validHeaderName := regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
|
||||||
|
return validHeaderName.MatchString(headerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isHeaderContinuation checks if a line is a header continuation
|
||||||
|
func (v *InputValidator) isHeaderContinuation(line string) bool {
|
||||||
|
return strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
// looksLikeEncodedContent checks if a line looks like encoded content that should be skipped
|
||||||
|
func (v *InputValidator) looksLikeEncodedContent(line string) bool {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// Very long lines without spaces are likely encoded
|
||||||
|
if len(line) > 100 && !strings.Contains(line, " ") && !strings.Contains(line, ":") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64-like patterns (long strings of alphanumeric + / + =)
|
||||||
|
if len(line) > 50 {
|
||||||
|
base64Pattern := regexp.MustCompile(`^[A-Za-z0-9+/=\s]+$`)
|
||||||
|
if base64Pattern.MatchString(line) && !strings.Contains(line, ":") {
|
||||||
|
// Count alphanumeric characters vs spaces
|
||||||
|
alphanumeric := regexp.MustCompile(`[A-Za-z0-9+/=]`).FindAllString(line, -1)
|
||||||
|
if float64(len(alphanumeric)) > float64(len(line))*0.8 { // More than 80% alphanumeric
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDNSQuery validates DNS query input
|
||||||
|
func (v *InputValidator) ValidateDNSQuery(query string) (string, error) {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
|
||||||
|
if query == "" {
|
||||||
|
return "", &ValidationError{Field: "query", Message: "DNS query cannot be empty"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query) > 253 { // Maximum domain name length
|
||||||
|
return "", &ValidationError{Field: "query", Message: "DNS query too long"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow only valid DNS characters: letters, numbers, dots, hyphens, and colons (for IPv6)
|
||||||
|
validDNS := regexp.MustCompile(`^[a-zA-Z0-9\.\-:]+$`)
|
||||||
|
if !validDNS.MatchString(query) {
|
||||||
|
return "", &ValidationError{Field: "query", Message: "DNS query contains invalid characters"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIntRange validates integer input within a range
|
||||||
|
func (v *InputValidator) ValidateIntRange(value, min, max int, fieldName string) error {
|
||||||
|
if value < min || value > max {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: fmt.Sprintf("Value must be between %d and %d", min, max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientIP extracts the real client IP from request headers
|
||||||
|
func (v *InputValidator) GetClientIP(r *http.Request) string {
|
||||||
|
// Check X-Forwarded-For header first
|
||||||
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||||
|
// Take the first IP in the list
|
||||||
|
ips := strings.Split(xff, ",")
|
||||||
|
if len(ips) > 0 {
|
||||||
|
ip := strings.TrimSpace(ips[0])
|
||||||
|
if ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check X-Real-IP header
|
||||||
|
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||||
|
return strings.TrimSpace(xri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to RemoteAddr
|
||||||
|
ip := r.RemoteAddr
|
||||||
|
if colonIndex := strings.LastIndex(ip, ":"); colonIndex != -1 {
|
||||||
|
ip = ip[:colonIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove IPv6 brackets
|
||||||
|
ip = strings.Trim(ip, "[]")
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationError represents a validation error
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ValidationError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidationError checks if an error is a validation error
|
||||||
|
func IsValidationError(err error) bool {
|
||||||
|
_, ok := err.(*ValidationError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ document.getElementById('dnsForm').addEventListener('submit', function() {
|
|||||||
<p><small>Time: ${timestamp} | Server: ${server || 'Default'}</small></p>
|
<p><small>Time: ${timestamp} | Server: ${server || 'Default'}</small></p>
|
||||||
<pre>${data}</pre>
|
<pre>${data}</pre>
|
||||||
`;
|
`;
|
||||||
document.getElementById('dnsResults').appendChild(resultDiv);
|
document.getElementById('dnsResults').insertBefore(resultDiv, document.getElementById('dnsResults').firstChild);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
@@ -102,7 +102,7 @@ document.getElementById('dnsForm').addEventListener('submit', function() {
|
|||||||
<h3>Error querying ${query}</h3>
|
<h3>Error querying ${query}</h3>
|
||||||
<pre>Error: ${error.message}</pre>
|
<pre>Error: ${error.message}</pre>
|
||||||
`;
|
`;
|
||||||
document.getElementById('dnsResults').appendChild(resultDiv);
|
document.getElementById('dnsResults').insertBefore(resultDiv, document.getElementById('dnsResults').firstChild);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
<h1>Email Header Analyzer</h1>
|
<h1>Email Header Analyzer</h1>
|
||||||
{{if not .From}}
|
{{if not .From}}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<textarea name="headers" placeholder="Paste email headers here..."></textarea>
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
<textarea name="headers" placeholder="Paste email headers here..." required></textarea>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Analyze Headers</button>
|
<button type="submit">Analyze Headers</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
<div class="password-generator">
|
<div class="password-generator">
|
||||||
<h1>🔐 Password Generator</h1>
|
<h1>🔐 Password Generator</h1>
|
||||||
|
|
||||||
|
<!-- Hidden CSRF token for API calls -->
|
||||||
|
<input type="hidden" id="csrfToken" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
<div class="tab-buttons">
|
<div class="tab-buttons">
|
||||||
<button class="tab-btn" id="randomTab">Random Password</button>
|
<button class="tab-btn" id="randomTab">Random Password</button>
|
||||||
<button class="tab-btn active" id="passphraseTab">Passphrase</button>
|
<button class="tab-btn active" id="passphraseTab">Passphrase</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user