@@ -372,6 +372,17 @@ func RunScan(targetURL string, informativeOnly bool) ([]models.Leak, models.Tech
372372 analyzeContent (r .Request .URL .String (), content , & leaks , & leaksMutex )
373373 }()
374374 }
375+
376+ // Frontend security analysis (headers + body)
377+ if ! informativeOnly {
378+ bodyCopy := make ([]byte , len (r .Body ))
379+ copy (bodyCopy , r .Body )
380+ wg .Add (1 )
381+ go func () {
382+ defer wg .Done ()
383+ analyzeFrontendSecurity (r .Request .URL .String (), * r .Headers , bodyCopy , & leaks , & leaksMutex )
384+ }()
385+ }
375386 })
376387
377388 // Setup error handling
@@ -1223,6 +1234,18 @@ func isCrawlableResourceRef(raw string) bool {
12231234
12241235var rxAPIKeyHeader = regexp .MustCompile (`(?i)(?:api[_-]?key|api[_-]?token|auth[_-]?token|x[_-]?api[_-]?key|x[_-]?auth[_-]?token)` )
12251236
1237+ var rxScriptTag = regexp .MustCompile (`(?i)<script\s[^>]*src\s*=\s*["']([^"']+)["'][^>]*>` )
1238+ var rxStyleLink = regexp .MustCompile (`(?i)<link\s[^>]*rel\s*=\s*["']stylesheet["'][^>]*href\s*=\s*["']([^"']+)["'][^>]*>` )
1239+ var rxXSSSink = regexp .MustCompile (`(?i)(?:\.innerHTML\s*=|\.outerHTML\s*=|document\.write\s*\(|eval\s*\(|document\.open\s*\(|\.insertAdjacentHTML\s*\()` )
1240+ var rxOpenRedirect = regexp .MustCompile (`(?i)(?:window\.)?location(?:\.href)?\s*=\s*.*?(?:params|query|search|url|redirect|next|return)` )
1241+ var rxInsecureFormAction = regexp .MustCompile (`(?i)<form\s[^>]*action\s*=\s*["']http://[^"']+["']` )
1242+ var rxCDNjQuery = regexp .MustCompile (`(?i)(?:cdnjs\.cloudflare|cdn\.jsdelivr|ajax\.googleapis|code\.jquery)\.(?:com|net).*jquery[-.\/@][0-9]+\.[0-9]+\.[0-9]+` )
1243+ var rxCDNAngular = regexp .MustCompile (`(?i)(?:cdnjs\.cloudflare|cdn\.jsdelivr|ajax\.googleapis|unpkg)\.(?:com|net).*angular(?:\.js)?[-.\/@][0-9]+\.[0-9]+\.[0-9]+` )
1244+ var rxCDNReact = regexp .MustCompile (`(?i)(?:cdnjs\.cloudflare|cdn\.jsdelivr|unpkg)\.(?:com|net).*react[-.\/@][0-9]+\.[0-9]+\.[0-9]+` )
1245+ var rxCDNVue = regexp .MustCompile (`(?i)(?:cdnjs\.cloudflare|cdn\.jsdelivr|unpkg)\.(?:com|net).*vue[-.\/@][0-9]+\.[0-9]+\.[0-9]+` )
1246+ var rxCDNBootstrap = regexp .MustCompile (`(?i)(?:cdnjs\.cloudflare|cdn\.jsdelivr|maxcdn|getbootstrap|bootstrapcdn)\.(?:com|net).*bootstrap[-.\/@][0-9]+\.[0-9]+\.[0-9]+` )
1247+ var rxCDNLodash = regexp .MustCompile (`(?i)(?:cdnjs\.cloudflare|cdn\.jsdelivr|unpkg)\.(?:com|net).*lodash(?:\.js)?[-.\/@][0-9]+\.[0-9]+\.[0-9]+` )
1248+
12261249func analyzeHeaders (sourceURL string , headers * http.Header , leaks * []models.Leak , mutex * sync.Mutex ) {
12271250 var localLeaks []models.Leak
12281251 for k , vals := range * headers {
@@ -2229,6 +2252,219 @@ func mustParseURL(raw string) *url.URL {
22292252 return u
22302253}
22312254
2255+ func analyzeFrontendSecurity (sourceURL string , headers http.Header , body []byte , leaks * []models.Leak , mutex * sync.Mutex ) {
2256+ var localLeaks []models.Leak
2257+
2258+ ctype := headers .Get ("Content-Type" )
2259+ bodyStr := string (body )
2260+
2261+ analyzeCSP (headers , sourceURL , & localLeaks )
2262+ analyzeCORS (headers , sourceURL , & localLeaks )
2263+ analyzeCookieSecurity (headers , sourceURL , & localLeaks )
2264+
2265+ if strings .Contains (ctype , "text/html" ) {
2266+ analyzeMissingSRI (bodyStr , sourceURL , & localLeaks )
2267+ analyzeInsecureForms (bodyStr , sourceURL , & localLeaks )
2268+ analyzeVulnerableCDN (bodyStr , sourceURL , & localLeaks )
2269+ }
2270+
2271+ if strings .Contains (ctype , "text/html" ) || strings .Contains (ctype , "javascript" ) {
2272+ analyzeXSSSinks (bodyStr , sourceURL , & localLeaks )
2273+ analyzeOpenRedirect (bodyStr , sourceURL , & localLeaks )
2274+ }
2275+
2276+ if len (localLeaks ) > 0 {
2277+ localLeaks = dedupeLeaks (localLeaks )
2278+ mutex .Lock ()
2279+ * leaks = append (* leaks , localLeaks ... )
2280+ mutex .Unlock ()
2281+ }
2282+ }
2283+
2284+ func analyzeCSP (headers http.Header , sourceURL string , localLeaks * []models.Leak ) {
2285+ csp := headers .Get ("Content-Security-Policy" )
2286+ if csp == "" {
2287+ * localLeaks = append (* localLeaks , models.Leak {
2288+ LeakType : models .LeakTypeWeakCSP ,
2289+ SourceURL : sourceURL ,
2290+ GravityScore : 5.0 ,
2291+ Snippet : "Content-Security-Policy header is missing" ,
2292+ })
2293+ return
2294+ }
2295+ cspLower := strings .ToLower (csp )
2296+
2297+ if strings .Contains (cspLower , "unsafe-inline" ) {
2298+ * localLeaks = append (* localLeaks , models.Leak {
2299+ LeakType : models .LeakTypeWeakCSP ,
2300+ SourceURL : sourceURL ,
2301+ GravityScore : 7.0 ,
2302+ Snippet : "CSP allows unsafe-inline: " + truncate (csp , 100 ),
2303+ })
2304+ }
2305+ if strings .Contains (cspLower , "unsafe-eval" ) {
2306+ * localLeaks = append (* localLeaks , models.Leak {
2307+ LeakType : models .LeakTypeWeakCSP ,
2308+ SourceURL : sourceURL ,
2309+ GravityScore : 6.5 ,
2310+ Snippet : "CSP allows unsafe-eval: " + truncate (csp , 100 ),
2311+ })
2312+ }
2313+ if ! strings .Contains (cspLower , "object-src" ) {
2314+ * localLeaks = append (* localLeaks , models.Leak {
2315+ LeakType : models .LeakTypeWeakCSP ,
2316+ SourceURL : sourceURL ,
2317+ GravityScore : 5.5 ,
2318+ Snippet : "CSP missing object-src directive: " + truncate (csp , 100 ),
2319+ })
2320+ }
2321+ }
2322+
2323+ func analyzeCORS (headers http.Header , sourceURL string , localLeaks * []models.Leak ) {
2324+ origin := headers .Get ("Access-Control-Allow-Origin" )
2325+ creds := headers .Get ("Access-Control-Allow-Credentials" )
2326+
2327+ if origin == "*" && strings .EqualFold (creds , "true" ) {
2328+ * localLeaks = append (* localLeaks , models.Leak {
2329+ LeakType : models .LeakTypeCORMisconfig ,
2330+ SourceURL : sourceURL ,
2331+ GravityScore : 8.0 ,
2332+ Snippet : "Access-Control-Allow-Origin: * with Allow-Credentials: true" ,
2333+ })
2334+ } else if origin == "*" {
2335+ * localLeaks = append (* localLeaks , models.Leak {
2336+ LeakType : models .LeakTypeCORMisconfig ,
2337+ SourceURL : sourceURL ,
2338+ GravityScore : 5.0 ,
2339+ Snippet : "Access-Control-Allow-Origin: * (wildcard)" ,
2340+ })
2341+ }
2342+ }
2343+
2344+ func analyzeCookieSecurity (headers http.Header , sourceURL string , localLeaks * []models.Leak ) {
2345+ cookies := cookiesFromHeader (& headers )
2346+ for _ , c := range cookies {
2347+ if c == nil {
2348+ continue
2349+ }
2350+ if ! c .HttpOnly {
2351+ * localLeaks = append (* localLeaks , models.Leak {
2352+ LeakType : models .LeakTypeCookieHardening ,
2353+ SourceURL : sourceURL ,
2354+ GravityScore : 6.0 ,
2355+ Snippet : fmt .Sprintf ("Cookie %q missing HttpOnly flag" , c .Name ),
2356+ })
2357+ }
2358+ if ! c .Secure {
2359+ * localLeaks = append (* localLeaks , models.Leak {
2360+ LeakType : models .LeakTypeCookieHardening ,
2361+ SourceURL : sourceURL ,
2362+ GravityScore : 5.5 ,
2363+ Snippet : fmt .Sprintf ("Cookie %q missing Secure flag" , c .Name ),
2364+ })
2365+ }
2366+ if c .SameSite == http .SameSiteDefaultMode {
2367+ * localLeaks = append (* localLeaks , models.Leak {
2368+ LeakType : models .LeakTypeCookieHardening ,
2369+ SourceURL : sourceURL ,
2370+ GravityScore : 4.0 ,
2371+ Snippet : fmt .Sprintf ("Cookie %q missing SameSite attribute" , c .Name ),
2372+ })
2373+ }
2374+ }
2375+ }
2376+
2377+ func analyzeMissingSRI (body , sourceURL string , localLeaks * []models.Leak ) {
2378+ for _ , match := range rxScriptTag .FindAllStringSubmatch (body , - 1 ) {
2379+ if len (match ) < 2 {
2380+ continue
2381+ }
2382+ fullTag := match [0 ]
2383+ src := match [1 ]
2384+ if ! strings .Contains (fullTag , "integrity=" ) {
2385+ * localLeaks = append (* localLeaks , models.Leak {
2386+ LeakType : models .LeakTypeMissingSRI ,
2387+ SourceURL : sourceURL ,
2388+ GravityScore : 4.0 ,
2389+ Snippet : fmt .Sprintf ("Script without integrity attribute: %s" , truncate (src , 80 )),
2390+ })
2391+ }
2392+ }
2393+ for _ , match := range rxStyleLink .FindAllStringSubmatch (body , - 1 ) {
2394+ if len (match ) < 2 {
2395+ continue
2396+ }
2397+ fullTag := match [0 ]
2398+ href := match [1 ]
2399+ if ! strings .Contains (fullTag , "integrity=" ) {
2400+ * localLeaks = append (* localLeaks , models.Leak {
2401+ LeakType : models .LeakTypeMissingSRI ,
2402+ SourceURL : sourceURL ,
2403+ GravityScore : 3.0 ,
2404+ Snippet : fmt .Sprintf ("Stylesheet without integrity attribute: %s" , truncate (href , 80 )),
2405+ })
2406+ }
2407+ }
2408+ }
2409+
2410+ func analyzeXSSSinks (body , sourceURL string , localLeaks * []models.Leak ) {
2411+ for _ , match := range rxXSSSink .FindAllString (body , - 1 ) {
2412+ * localLeaks = append (* localLeaks , models.Leak {
2413+ LeakType : models .LeakTypeXSSSink ,
2414+ SourceURL : sourceURL ,
2415+ GravityScore : 8.0 ,
2416+ Snippet : fmt .Sprintf ("XSS sink detected: %s" , truncate (strings .TrimSpace (match ), 60 )),
2417+ })
2418+ }
2419+ }
2420+
2421+ func analyzeOpenRedirect (body , sourceURL string , localLeaks * []models.Leak ) {
2422+ for _ , match := range rxOpenRedirect .FindAllString (body , - 1 ) {
2423+ * localLeaks = append (* localLeaks , models.Leak {
2424+ LeakType : models .LeakTypeOpenRedirect ,
2425+ SourceURL : sourceURL ,
2426+ GravityScore : 7.0 ,
2427+ Snippet : fmt .Sprintf ("Potential open redirect: %s" , truncate (strings .TrimSpace (match ), 80 )),
2428+ })
2429+ }
2430+ }
2431+
2432+ func analyzeInsecureForms (body , sourceURL string , localLeaks * []models.Leak ) {
2433+ for _ , match := range rxInsecureFormAction .FindAllString (body , - 1 ) {
2434+ * localLeaks = append (* localLeaks , models.Leak {
2435+ LeakType : models .LeakTypeInsecureForm ,
2436+ SourceURL : sourceURL ,
2437+ GravityScore : 6.0 ,
2438+ Snippet : fmt .Sprintf ("Insecure form action (HTTP): %s" , truncate (strings .TrimSpace (match ), 100 )),
2439+ })
2440+ }
2441+ }
2442+
2443+ func analyzeVulnerableCDN (body , sourceURL string , localLeaks * []models.Leak ) {
2444+ checks := []struct {
2445+ rx * regexp.Regexp
2446+ lib string
2447+ score float64
2448+ }{
2449+ {rxCDNjQuery , "jQuery" , 5.0 },
2450+ {rxCDNAngular , "Angular" , 4.5 },
2451+ {rxCDNReact , "React" , 4.0 },
2452+ {rxCDNVue , "Vue" , 4.0 },
2453+ {rxCDNBootstrap , "Bootstrap" , 4.0 },
2454+ {rxCDNLodash , "Lodash" , 4.0 },
2455+ }
2456+ for _ , check := range checks {
2457+ for _ , match := range check .rx .FindAllString (body , - 1 ) {
2458+ * localLeaks = append (* localLeaks , models.Leak {
2459+ LeakType : models .LeakTypeVulnCDN ,
2460+ SourceURL : sourceURL ,
2461+ GravityScore : check .score ,
2462+ Snippet : fmt .Sprintf ("Known %s CDN dependency (check version for known CVEs): %s" , check .lib , truncate (match , 80 )),
2463+ })
2464+ }
2465+ }
2466+ }
2467+
22322468func applyBrowserHeaders (req * http.Request ) {
22332469 // A conservative Chrome-like header set (no cookies by default).
22342470 req .Header .Set ("User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" )
0 commit comments