Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ require (
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/odvcencio/gotreesitter v0.5.3-0.20260227083844-016686287e7c // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/odvcencio/gotreesitter v0.5.2 h1:t3rlN8w9kWxHS9IZbMhE8ItO/IkCLQn9755RLAC49VI=
github.com/odvcencio/gotreesitter v0.5.2/go.mod h1:CEjhRXmfP38AHqxwWxNcLaMsLQ3YoGMrwNGle1huZdU=
github.com/odvcencio/gotreesitter v0.5.3-0.20260227072035-f76cb0d8aa95 h1:lMZGc05VRFVD5LU6CIzx3Rpn6clywgN7r76oVntUcnA=
github.com/odvcencio/gotreesitter v0.5.3-0.20260227072035-f76cb0d8aa95/go.mod h1:CEjhRXmfP38AHqxwWxNcLaMsLQ3YoGMrwNGle1huZdU=
github.com/odvcencio/gotreesitter v0.5.3-0.20260227083844-016686287e7c h1:93JqddF+SdRjfiYqef+/z3Bo1NCB8s1llaHVc9JdnKM=
github.com/odvcencio/gotreesitter v0.5.3-0.20260227083844-016686287e7c/go.mod h1:CEjhRXmfP38AHqxwWxNcLaMsLQ3YoGMrwNGle1huZdU=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
Expand Down
100 changes: 100 additions & 0 deletions pkg/fuzz/analyzers/xss/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package xss

import (
"crypto/rand"
"io"
"strings"

"github.com/pkg/errors"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
)

// Analyzer is an XSS context analyzer for the fuzzer.
// It injects a canary value and uses gotreesitter AST parsing to determine
// the precise HTML/JS/CSS context of each reflection point.
type Analyzer struct{}

var _ analyzers.Analyzer = &Analyzer{}

const defaultMaxResponseBodySize = 10 * 1024 * 1024 // 10 MB

func init() {
analyzers.RegisterAnalyzer("xss_context", &Analyzer{})
}

// Name returns the name of this analyzer.
func (a *Analyzer) Name() string {
return "xss_context"
}

// ApplyInitialTransformation applies payload transformations.
// For XSS context analysis, we don't transform the initial payload since
// we generate our own canary in Analyze().
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
return analyzers.ApplyPayloadTransformations(data)
}

// Analyze sends a request with a unique canary value and determines the
// XSS context of all reflection points using AST parsing.
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
canary := generateCanary()

gr := options.FuzzGenerated
if err := gr.Component.SetValue(gr.Key, canary); err != nil {
return false, "", errors.Wrap(err, "could not set canary value")
}

rebuilt, err := gr.Component.Rebuild()
if err != nil {
return false, "", errors.Wrap(err, "could not rebuild request with canary")
}

resp, err := options.HttpClient.Do(rebuilt)
if err != nil {
return false, "", errors.Wrap(err, "could not send canary request")
}
defer resp.Body.Close()

body, err := io.ReadAll(io.LimitReader(resp.Body, defaultMaxResponseBodySize))
if err != nil {
return false, "", errors.Wrap(err, "could not read response body")
}

// Quick check: if canary is not reflected at all, no XSS
if !strings.Contains(string(body), canary) {
return false, "", nil
}

// Parse HTML and find all reflection contexts
points := findReflections(body, canary)
if len(points) == 0 {
return false, "", nil
}

// Build details string listing all unique contexts
seen := make(map[XSSContext]bool)
var details []string
for _, p := range points {
if !seen[p.Context] {
seen[p.Context] = true
details = append(details, p.Context.String())
}
}

return true, strings.Join(details, ", "), nil
}

// generateCanary creates a unique canary string unlikely to appear in natural content.
// Format: "gtss" + 8 random alphanumeric characters.
func generateCanary() string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
// Fallback to a fixed canary if crypto/rand fails (extremely unlikely)
return "gtss00000000"
}
for i := range b {
b[i] = charset[b[i]%byte(len(charset))]
}
return "gtss" + string(b)
}
Loading