/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package scan import ( "net/http" "net/http/httptest" "testing" "time" ) // genericDefaultPanelTypes are the sqlAdminPaths entries with no product-specific // case in isAdminPanel, so they fall through to the default keyword branch. var genericDefaultPanelTypes = []string{ "SQL Interface", "Database Interface", "Database Admin", "MySQL Admin", "SQL Manager", "WebSQL", "SQLWeb", "MongoDB Interface", "Redis Interface", } // an ordinary javascript page is not a database admin panel. "query" used to // match the default branch via jQuery/querySelector, flagging every js site. func TestIsAdminPanel_GenericJSPageNotFlagged(t *testing.T) { pages := []struct{ name, body string }{ {"jquery script tag", ``}, {"querySelector call", ``}, {"jquery invocation", ``}, {"search query word", "
"}, {"graphql query const", ``}, } for _, p := range pages { for _, pt := range genericDefaultPanelTypes { if isAdminPanel(p.body, pt) { t.Errorf("%s wrongly flagged as %q admin panel", p.name, pt) } } } } // dropping "query" must not reduce recall: real db interfaces still match via // the sibling keywords (database/sql/mysql/postgresql/mongodb). func TestIsAdminPanel_RealGenericPanelsStillDetected(t *testing.T) { cases := []struct{ name, body string }{ {"database manager", "Database Manager"}, {"sql console", "

SQL Console

"}, {"mysql admin", "MySQL Administration"}, {"postgresql browser", "
PostgreSQL database browser
"}, {"mongodb express", "mongodb express"}, {"sql query interface", "
SQL Query Interface
"}, } for _, c := range cases { if !isAdminPanel(c.body, "Database Interface") { t.Errorf("%s should still be detected as a database interface", c.name) } } } // the precise change: a lone "query" no longer triggers, but "query" alongside // a db keyword still does, carried by the sibling. func TestIsAdminPanel_QueryRemovalPrecise(t *testing.T) { if isAdminPanel("Query Console", "Database Interface") { t.Error(`lone "query" should no longer trigger the default branch`) } if !isAdminPanel("SQL Query Tool", "Database Interface") { t.Error(`"query" with "sql" should still detect via "sql"`) } } // the default-branch change must not disturb the product-specific cases. func TestIsAdminPanel_ExplicitCasesUnaffected(t *testing.T) { cases := []struct { panelType string body string want bool }{ {"phpMyAdmin", "phpMyAdmin", true}, {"phpMyAdmin", "", true}, {"phpMyAdmin", "Home", false}, {"Adminer", "Adminer", true}, {"Adminer", "nothing relevant", false}, {"pgAdmin", "pgAdmin 4", true}, {"phpPgAdmin", "

phpPgAdmin

", true}, {"RockMongo", "RockMongo", true}, {"Redis Commander", "Redis Commander", true}, {"phpRedisAdmin", "

phpRedisAdmin

", true}, {"phpMyAdmin", ``, false}, } for _, c := range cases { if got := isAdminPanel(c.body, c.panelType); got != c.want { t.Errorf("isAdminPanel(%q, %q) = %v, want %v", c.body, c.panelType, got, c.want) } } } // end to end: a catch-all that serves a jquery page at every path (the common // soft-404-as-200 case) must not yield any admin-panel finding. func TestSQL_JQueryCatchAllNotReported(t *testing.T) { jq := `

Welcome

` srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(jq)) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", false) if err != nil { t.Fatalf("SQL: %v", err) } // SQL returns a nil result when nothing is found, which is the pass case here. if result != nil && len(result.AdminPanels) != 0 { t.Errorf("jquery catch-all produced %d admin-panel finding(s): %+v", len(result.AdminPanels), result.AdminPanels) } } // end to end: a real phpMyAdmin install is still reported. func TestSQL_RealPhpMyAdminReported(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/phpmyadmin/" { _, _ = w.Write([]byte("phpMyAdmin")) return } w.WriteHeader(http.StatusNotFound) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", false) if err != nil { t.Fatalf("SQL: %v", err) } if result == nil { t.Fatal("expected a phpMyAdmin finding, got nil result") } found := false for _, p := range result.AdminPanels { if p.Type == "phpMyAdmin" { found = true } } if !found { t.Errorf("real phpMyAdmin not reported; panels=%+v", result.AdminPanels) } } // end to end: a genuine generic db interface (db-topical body at a db path) is // still reported, so the change did not over-tighten the default branch. func TestSQL_RealGenericPanelReported(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/db/" { _, _ = w.Write([]byte("Database ManagerMySQL server status")) return } w.WriteHeader(http.StatusNotFound) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", false) if err != nil { t.Fatalf("SQL: %v", err) } if result == nil || len(result.AdminPanels) == 0 { t.Error("a real database interface at /db/ should still be reported") } }