diff --git a/cmd/root.go b/cmd/root.go index 684a21f5..53cafc14 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -356,10 +356,10 @@ func initConfig() { SkipDiscovery: args.SkipDiscovery, HarFilePath: args.HarFilePath, // Issue #695 and #764 flags - DetailedAnalysis: args.DetailedAnalysis, - FastScan: args.FastScan, - MagicCharTest: args.MagicCharTest, - ContextAware: args.ContextAware, + DetailedAnalysis: args.DetailedAnalysis, + FastScan: args.FastScan, + MagicCharTest: args.MagicCharTest, + ContextAware: args.ContextAware, } // If configuration file was loaded, apply values from it for options not specified via CLI diff --git a/cmd/server.go b/cmd/server.go index c61e3d16..dd0a324d 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -7,10 +7,10 @@ import ( ) // Command-line flags for server configuration -var port int // Port to bind the server to -var host, serverType, apiKey string // Host address, server type, and API Key -var allowedOrigins []string // Allowed origins for CORS -var jsonp bool // Enable JSONP responses +var port int // Port to bind the server to +var host, serverType, apiKey string // Host address, server type, and API Key +var allowedOrigins []string // Allowed origins for CORS +var jsonp bool // Enable JSONP responses // serverCmd represents the server command for starting API servers var serverCmd = &cobra.Command{ diff --git a/cmd/utils.go b/cmd/utils.go index e157d00b..be681276 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -12,8 +12,6 @@ import ( // This file contains utility functions for managing command-line help output // and other shared functionality used across the different commands - - // SubCommandCustomHelpFunc provides a custom help formatter for subcommands // This function is shared across all subcommands to ensure consistent help display // It leverages templates to create a more organized and user-friendly help output diff --git a/internal/payload/remote_test.go b/internal/payload/remote_test.go index aa505899..d5c6f490 100644 --- a/internal/payload/remote_test.go +++ b/internal/payload/remote_test.go @@ -75,9 +75,9 @@ func Test_getAssetHahwul(t *testing.T) { apiContent: validAssetJSON, dataContent: emptyPayloadData, expectedPayloads: []string{}, // splitLines on "" might give {""} or {}, depends on impl. - // current splitLines gives empty slice for empty string. - expectedLine: "2", - expectedSize: "10 bytes", + // current splitLines gives empty slice for empty string. + expectedLine: "2", + expectedSize: "10 bytes", }, { name: "malformed JSON (unmarshal error)", @@ -92,7 +92,7 @@ func Test_getAssetHahwul(t *testing.T) { // originalHTTPGet := httpGet // No longer needed // defer func() { httpGet = originalHTTPGet }() - originalBaseURL := assetHahwulBaseURL // Store original base URL + originalBaseURL := assetHahwulBaseURL // Store original base URL defer func() { assetHahwulBaseURL = originalBaseURL }() // Restore it after all tests in this function for _, tt := range tests { @@ -186,7 +186,7 @@ func TestGetPortswiggerPayload(t *testing.T) { } else if strings.HasSuffix(r.URL.Path, "xss-portswigger.txt") { fmt.Fprintln(w, payloadData) } else { - http.NotFound(w,r) + http.NotFound(w, r) } })) assetHahwulBaseURL = mockServer.URL @@ -221,7 +221,7 @@ func TestGetPayloadBoxPayload(t *testing.T) { } else if strings.HasSuffix(r.URL.Path, "xss-payloadbox.txt") { fmt.Fprintln(w, payloadData) } else { - http.NotFound(w,r) + http.NotFound(w, r) } })) assetHahwulBaseURL = mockServer.URL @@ -256,7 +256,7 @@ func TestGetBurpWordlist(t *testing.T) { } else if strings.HasSuffix(r.URL.Path, "wl-params.txt") { fmt.Fprintln(w, payloadData) } else { - http.NotFound(w,r) + http.NotFound(w, r) } })) assetHahwulBaseURL = mockServer.URL @@ -290,7 +290,7 @@ func TestGetAssetnoteWordlist(t *testing.T) { } else if strings.HasSuffix(r.URL.Path, "wl-assetnote-params.txt") { fmt.Fprintln(w, payloadData) } else { - http.NotFound(w,r) + http.NotFound(w, r) } })) assetHahwulBaseURL = mockServer.URL @@ -469,103 +469,103 @@ type testAsset struct { } func TestGetAssetHahwul_JsonUnmarshalError(t *testing.T) { - // Simulate a scenario where JSON unmarshalling fails - mockAPIContent := `{"line": 123, "size": "not a string field for Line"}` // Invalid: line is number - mockDataContent := "payload1\npayload2" - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasSuffix(r.URL.Path, ".json") { // Simplified to any .json for this test's purpose - w.Header().Set("Content-Type", "application/json") - fmt.Fprintln(w, mockAPIContent) - } else { - fmt.Fprintln(w, mockDataContent) - } - })) + // Simulate a scenario where JSON unmarshalling fails + mockAPIContent := `{"line": 123, "size": "not a string field for Line"}` // Invalid: line is number + mockDataContent := "payload1\npayload2" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, ".json") { // Simplified to any .json for this test's purpose + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, mockAPIContent) + } else { + fmt.Fprintln(w, mockDataContent) + } + })) originalBaseURL := assetHahwulBaseURL assetHahwulBaseURL = server.URL - defer func() { + defer func() { assetHahwulBaseURL = originalBaseURL server.Close() }() - // The actual Asset struct in remote.go is { Line string, Size string } - // If json.Unmarshal receives a number for 'Line', it will error out. - // The function getAssetHahwul will then proceed with default/empty Asset values. - payloads, line, size := getAssetHahwul("api.json", "data.txt") // Endpoints are relative to assets.hahwul.com - - // Payloads should still be fetched - expectedPayloads := []string{"payload1", "payload2"} - if !equalSlices(payloads, expectedPayloads) { - t.Errorf("getAssetHahwul with JSON unmarshal error, payloads = %v, want %v", payloads, expectedPayloads) - } - // Line should be empty due to unmarshal error for the 'Line' field (type mismatch) - if line != "" { - t.Errorf("getAssetHahwul with JSON unmarshal error, line = %q, want \"\"", line) - } - // Size should be "not a string field for Line" as it's a valid string in JSON and struct - if size != "not a string field for Line" { - t.Errorf("getAssetHahwul with JSON unmarshal error, size = %q, want %q", size, "not a string field for Line") - } + // The actual Asset struct in remote.go is { Line string, Size string } + // If json.Unmarshal receives a number for 'Line', it will error out. + // The function getAssetHahwul will then proceed with default/empty Asset values. + payloads, line, size := getAssetHahwul("api.json", "data.txt") // Endpoints are relative to assets.hahwul.com + + // Payloads should still be fetched + expectedPayloads := []string{"payload1", "payload2"} + if !equalSlices(payloads, expectedPayloads) { + t.Errorf("getAssetHahwul with JSON unmarshal error, payloads = %v, want %v", payloads, expectedPayloads) + } + // Line should be empty due to unmarshal error for the 'Line' field (type mismatch) + if line != "" { + t.Errorf("getAssetHahwul with JSON unmarshal error, line = %q, want \"\"", line) + } + // Size should be "not a string field for Line" as it's a valid string in JSON and struct + if size != "not a string field for Line" { + t.Errorf("getAssetHahwul with JSON unmarshal error, size = %q, want %q", size, "not a string field for Line") + } } func TestGetAssetHahwul_HttpErrorOnApi(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasSuffix(r.URL.Path, "someapi.json") { // Target specific API endpoint for error - http.Error(w, "API down", http.StatusServiceUnavailable) - } else { - fmt.Fprintln(w, "somedata") // Or handle other requests if necessary - } - })) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "someapi.json") { // Target specific API endpoint for error + http.Error(w, "API down", http.StatusServiceUnavailable) + } else { + fmt.Fprintln(w, "somedata") // Or handle other requests if necessary + } + })) originalBaseURL := assetHahwulBaseURL assetHahwulBaseURL = server.URL - defer func() { + defer func() { assetHahwulBaseURL = originalBaseURL server.Close() }() - payloads, line, size := getAssetHahwul("someapi.json", "somedata.txt") - - if len(payloads) != 0 { - t.Errorf("Expected empty payloads on API HTTP error, got %v", payloads) - } - if line != "" { - t.Errorf("Expected empty line on API HTTP error, got %s", line) - } - if size != "" { - t.Errorf("Expected empty size on API HTTP error, got %s", size) - } + payloads, line, size := getAssetHahwul("someapi.json", "somedata.txt") + + if len(payloads) != 0 { + t.Errorf("Expected empty payloads on API HTTP error, got %v", payloads) + } + if line != "" { + t.Errorf("Expected empty line on API HTTP error, got %s", line) + } + if size != "" { + t.Errorf("Expected empty size on API HTTP error, got %s", size) + } } func TestGetAssetHahwul_HttpErrorOnData(t *testing.T) { mockAPIContent := `{"line":"1","size":"1B"}` - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasSuffix(r.URL.Path, "somedata.txt") { // Target specific data endpoint for error - http.Error(w, "Data down", http.StatusServiceUnavailable) - } else if strings.HasSuffix(r.URL.Path, "someapi.json") { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "somedata.txt") { // Target specific data endpoint for error + http.Error(w, "Data down", http.StatusServiceUnavailable) + } else if strings.HasSuffix(r.URL.Path, "someapi.json") { w.Header().Set("Content-Type", "application/json") - fmt.Fprintln(w, mockAPIContent) + fmt.Fprintln(w, mockAPIContent) } else { - http.NotFound(w,r) + http.NotFound(w, r) } - })) + })) originalBaseURL := assetHahwulBaseURL assetHahwulBaseURL = server.URL - defer func() { + defer func() { assetHahwulBaseURL = originalBaseURL server.Close() }() - payloads, line, size := getAssetHahwul("someapi.json", "somedata.txt") - - if len(payloads) != 0 { - t.Errorf("Expected empty payloads on Data HTTP error, got %v", payloads) - } - // Line and Size might be populated from the API call if it succeeded before data call failed. - // The current implementation of getAssetHahwul returns empty for all if dataResp fails. - if line != "" { - t.Errorf("Expected empty line on Data HTTP error, got %s", line) - } - if size != "" { - t.Errorf("Expected empty size on Data HTTP error, got %s", size) - } + payloads, line, size := getAssetHahwul("someapi.json", "somedata.txt") + + if len(payloads) != 0 { + t.Errorf("Expected empty payloads on Data HTTP error, got %v", payloads) + } + // Line and Size might be populated from the API call if it succeeded before data call failed. + // The current implementation of getAssetHahwul returns empty for all if dataResp fails. + if line != "" { + t.Errorf("Expected empty line on Data HTTP error, got %s", line) + } + if size != "" { + t.Errorf("Expected empty size on Data HTTP error, got %s", size) + } } diff --git a/internal/utils/magic.go b/internal/utils/magic.go index 45fde19d..224da06e 100644 --- a/internal/utils/magic.go +++ b/internal/utils/magic.go @@ -15,12 +15,12 @@ var MagicCharacters = []string{ // ContextSpecificMagic contains magic characters for specific contexts var ContextSpecificMagic = map[string][]string{ "html": {"<", ">", "'", "\"", "&"}, - "js": {"'", "\"", ";", "{", "}", "(", ")", "`"}, - "css": {"{", "}", ";", ":", "/*", "*/", "'", "\""}, - "url": {"&", "=", "?", "#", "%", "+", " "}, + "js": {"'", "\"", ";", "{", "}", "(", ")", "`"}, + "css": {"{", "}", ";", ":", "/*", "*/", "'", "\""}, + "url": {"&", "=", "?", "#", "%", "+", " "}, "json": {"{", "}", "[", "]", ":", ",", "\""}, - "xml": {"<", ">", "&", "'", "\""}, - "sql": {"'", "\"", ";", "--", "/*", "*/", "(", ")"}, + "xml": {"<", ">", "&", "'", "\""}, + "sql": {"'", "\"", ";", "--", "/*", "*/", "(", ")"}, } // GenerateMagicCharacter generates a magic character based on context @@ -28,39 +28,39 @@ func GenerateMagicCharacter(context string) string { if chars, exists := ContextSpecificMagic[strings.ToLower(context)]; exists { return chars[rand.Intn(len(chars))] } - + return MagicCharacters[rand.Intn(len(MagicCharacters))] } // GenerateMagicString generates a string with multiple magic characters func GenerateMagicString(context string, length int) string { var result strings.Builder - + for i := 0; i < length; i++ { result.WriteString(GenerateMagicCharacter(context)) } - + return result.String() } // GetBypassHints returns WAF bypass hints for specific characters func GetBypassHints(char string) []string { bypassMap := map[string][]string{ - "<": {"<", "\\u003c", "\\x3c", "%3c", "\\074"}, - ">": {">", "\\u003e", "\\x3e", "%3e", "\\076"}, - "'": {"'", "\\u0027", "\\x27", "%27", "\\047"}, + "<": {"<", "\\u003c", "\\x3c", "%3c", "\\074"}, + ">": {">", "\\u003e", "\\x3e", "%3e", "\\076"}, + "'": {"'", "\\u0027", "\\x27", "%27", "\\047"}, "\"": {""", "\\u0022", "\\x22", "%22", "\\042"}, - "&": {"&", "\\u0026", "\\x26", "%26", "\\046"}, - "(": {"\\u0028", "\\x28", "%28", "\\050"}, - ")": {"\\u0029", "\\x29", "%29", "\\051"}, - ";": {"\\u003b", "\\x3b", "%3b", "\\073"}, - " ": {"%20", "+", "\\u0020", "\\x20"}, + "&": {"&", "\\u0026", "\\x26", "%26", "\\046"}, + "(": {"\\u0028", "\\x28", "%28", "\\050"}, + ")": {"\\u0029", "\\x29", "%29", "\\051"}, + ";": {"\\u003b", "\\x3b", "%3b", "\\073"}, + " ": {"%20", "+", "\\u0020", "\\x20"}, } - + if hints, exists := bypassMap[char]; exists { return hints } - + return []string{} } @@ -82,7 +82,7 @@ func DetectContext(response string, param string, value string) string { if strings.Contains(response, "alert('XSS')" } -} \ No newline at end of file +} diff --git a/internal/utils/magic_test.go b/internal/utils/magic_test.go index b9e34e40..64b7c793 100644 --- a/internal/utils/magic_test.go +++ b/internal/utils/magic_test.go @@ -220,9 +220,9 @@ func TestGenerateMagicString_EdgeCases(t *testing.T) { // Test with a context not in ContextSpecificMagic but also not in MagicCharacters (should default to MagicCharacters) // This scenario is implicitly covered by "unknown context" in TestGenerateMagicString, // but an explicit test ensures clarity. - rand.Seed(1) // Ensure predictability + rand.Seed(1) // Ensure predictability expectedFromAll := string(MagicCharacters[rand.Intn(len(MagicCharacters))]) // Get the first char based on seed - rand.Seed(1) // Reset seed for the actual call + rand.Seed(1) // Reset seed for the actual call gotUnknown := GenerateMagicCharacter("completely_new_context") if gotUnknown != expectedFromAll { t.Errorf("GenerateMagicCharacter() with completely new context = %s, want %s (based on seed)", gotUnknown, expectedFromAll) @@ -230,84 +230,84 @@ func TestGenerateMagicString_EdgeCases(t *testing.T) { } func TestDetectContext_MoreComplexScenarios(t *testing.T) { - tests := []struct { - name string - response string - param string - value string - want string - }{ - { - name: "JS context within HTML attribute", - response: `click`, - param: "p1", - value: "test_value", - want: "js", // Current logic will detect html first because of '<' and '>' around value if value is part of attribute name - // If value is *within* quotes of an event handler, it's more complex. - // The current DetectContext is simple and would likely return "html". - // This test highlights a limitation or area for improvement. - // For now, testing existing behavior. - }, - { - name: "Value embedded deep in JSON", - response: `{"data":{"items":[{"id":1,"name":"test_value"}]}}`, - param: "name", // Assuming param helps find the specific value location - value: "test_value", - want: "html", // Current DetectContext is too simple for deep JSON - }, - { - name: "Value in HTML comment", - response: `
Hello
`, - param: "userInput", - value: "test_value", - want: "html", // Comments are part of HTML structure - }, - { - name: "Value in script tag but it's a comment", - response: ``, - param: "p1", - value: "test_value", - want: "js", // Still inside `, + param: "p1", + value: "test_value", + want: "js", // Still inside click` - got = DetectContext(responseForTest, tt.param, tt.value) - // Even with click` + got = DetectContext(responseForTest, tt.param, tt.value) + // Even with `) matches := scriptRegex.FindAllStringSubmatch(htmlContent, -1) @@ -535,7 +535,7 @@ func ExtractJavaScript(htmlContent string) []string { jsCode = append(jsCode, match[1]) } } - + // Extract event handlers eventHandlers := []string{ "onclick", "onload", "onmouseover", "onmouseout", "onfocus", "onblur", @@ -546,14 +546,14 @@ func ExtractJavaScript(htmlContent string) []string { "onprogress", "onratechange", "onseeked", "onseeking", "onstalled", "onsuspend", "ontimeupdate", "onvolumechange", "onwaiting", } - + // Pre-compile regex patterns for better performance handlerPatterns := make([]*regexp.Regexp, len(eventHandlers)) for i, handler := range eventHandlers { pattern := fmt.Sprintf(`(?i)%s\s*=\s*["']([^"']*)["']`, handler) handlerPatterns[i] = regexp.MustCompile(pattern) } - + // Find all event handler matches for _, handlerRegex := range handlerPatterns { handlerMatches := handlerRegex.FindAllStringSubmatch(htmlContent, -1) @@ -563,7 +563,7 @@ func ExtractJavaScript(htmlContent string) []string { } } } - + // Extract javascript: URLs jsURLRegex := regexp.MustCompile(`(?i)javascript:([^"'\s>]*)`) matches = jsURLRegex.FindAllStringSubmatch(htmlContent, -1) @@ -572,7 +572,7 @@ func ExtractJavaScript(htmlContent string) []string { jsCode = append(jsCode, match[1]) } } - + return jsCode } @@ -580,9 +580,9 @@ func ExtractJavaScript(htmlContent string) []string { func AnalyzeDOMXSS(htmlContent string, targetURL string) []map[string]interface{} { detector := NewDOMXSSDetector() jsCodeBlocks := ExtractJavaScript(htmlContent) - + var allVulnerabilities []map[string]interface{} - + for i, jsCode := range jsCodeBlocks { vulns := detector.DetectDOMXSS(jsCode) for _, vuln := range vulns { @@ -592,6 +592,6 @@ func AnalyzeDOMXSS(htmlContent string, targetURL string) []map[string]interface{ allVulnerabilities = append(allVulnerabilities, vuln) } } - + return allVulnerabilities } diff --git a/pkg/scanning/scan.go b/pkg/scanning/scan.go index 7c797efb..e2b66074 100644 --- a/pkg/scanning/scan.go +++ b/pkg/scanning/scan.go @@ -44,7 +44,7 @@ type JSONInjector struct { // InjectIntoJSON recursively injects payload into JSON structure func (ji *JSONInjector) InjectIntoJSON(data interface{}, path string) []map[string]interface{} { var results []map[string]interface{} - + switch v := data.(type) { case map[string]interface{}: for key, value := range v { @@ -53,7 +53,7 @@ func (ji *JSONInjector) InjectIntoJSON(data interface{}, path string) []map[stri currentPath += "." } currentPath += key - + // Create a copy and inject payload modified := ji.deepCopy(data) if modifiedMap, ok := modified.(map[string]interface{}); ok { @@ -64,16 +64,16 @@ func (ji *JSONInjector) InjectIntoJSON(data interface{}, path string) []map[stri "key": key, }) } - + // Recursively process nested structures nestedResults := ji.InjectIntoJSON(value, currentPath) results = append(results, nestedResults...) } - + case []interface{}: for i, item := range v { currentPath := fmt.Sprintf("%s[%d]", path, i) - + // Create a copy and inject payload at array index modified := ji.deepCopy(data) if modifiedArray, ok := modified.([]interface{}); ok && i < len(modifiedArray) { @@ -84,13 +84,13 @@ func (ji *JSONInjector) InjectIntoJSON(data interface{}, path string) []map[stri "key": fmt.Sprintf("[%d]", i), }) } - + // Recursively process array items nestedResults := ji.InjectIntoJSON(item, currentPath) results = append(results, nestedResults...) } } - + return results } @@ -103,14 +103,14 @@ func (ji *JSONInjector) deepCopy(data interface{}) interface{} { copy[key] = ji.deepCopy(value) } return copy - + case []interface{}: copy := make([]interface{}, len(v)) for i, item := range v { copy[i] = ji.deepCopy(item) } return copy - + default: return v } @@ -123,11 +123,11 @@ func ParseJSONBody(body string, payload string) ([]map[string]interface{}, error if err != nil { return nil, err } - + injector := &JSONInjector{ Payload: payload, } - + return injector.InjectIntoJSON(jsonData, ""), nil } @@ -137,24 +137,24 @@ func CreateJSONRequest(originalReq *http.Request, modifiedJSON interface{}) (*ht if err != nil { return nil, err } - + // Create new request with modified JSON body newReq, err := http.NewRequest(originalReq.Method, originalReq.URL.String(), bytes.NewReader(jsonBytes)) if err != nil { return nil, err } - + // Copy headers for key, values := range originalReq.Header { for _, value := range values { newReq.Header.Add(key, value) } } - + // Set content type and length newReq.Header.Set("Content-Type", "application/json") newReq.Header.Set("Content-Length", strconv.Itoa(len(jsonBytes))) - + return newReq, nil } @@ -379,14 +379,14 @@ func generatePayloads(target string, options model.Options, policy map[string]st context = utils.DetectContext(v.ReflectedCode, k, "test") printing.DalLog("INFO", "Detected context for "+k+": "+context, options) } - + // Generate magic character payloads magicChars := []string{ utils.GenerateMagicCharacter(context), utils.GenerateMagicString(context, 3), utils.GenerateTestPayload(context), } - + for _, magicPayload := range magicChars { encoders := []string{NaN, urlEncode, htmlEncode} for _, encoder := range encoders {