Skip to content

Commit b1ff448

Browse files
committed
fix(frameworks): stop prose mentions from faking strapi and cakephp
The Strapi and CakePHP detectors keyed on a bare body word ("strapi", "cakephp"). MatchSignatures normalizes matched weight by total signature weight, so a lone body signature always scored a full 1.0, and the single-argmax reducer then let one prose mention on an unrelated page outrank and suppress the site's real, multi-marker framework. Key on the structural markers instead: the X-Powered-By poweredBy header for Strapi and the CAKEPHP session cookie for CakePHP, both header-only.
1 parent d52cd84 commit b1ff448

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

internal/scan/frameworks/detect_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,9 @@ func TestDetectFramework_StrapiFalsePositive(t *testing.T) {
908908

909909
func TestDetectFramework_Strapi(t *testing.T) {
910910
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
911+
w.Header().Set("X-Powered-By", "Strapi <strapi.io>")
911912
w.WriteHeader(http.StatusOK)
912-
w.Write([]byte(`<!DOCTYPE html><html><body><div>powered by strapi</div></body></html>`))
913+
w.Write([]byte(`<!DOCTYPE html><html><body><div>Welcome</div></body></html>`))
913914
}))
914915
defer server.Close()
915916

@@ -922,6 +923,57 @@ func TestDetectFramework_Strapi(t *testing.T) {
922923
}
923924
}
924925

926+
// a page that merely names the framework in prose (a listicle, a comparison)
927+
// must not be fingerprinted as that framework, as the old bare body words did.
928+
func TestDetectFramework_ProseMentionsAreNotDetections(t *testing.T) {
929+
cases := map[string]string{
930+
"Strapi": `<h1>Is Strapi Right For You?</h1><p>we compare strapi with other headless cms platforms.</p>`,
931+
"CakePHP": `<h1>Choosing a PHP framework</h1><p>cakephp, laravel and symfony all have strong communities.</p>`,
932+
}
933+
for name, body := range cases {
934+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
935+
w.WriteHeader(http.StatusOK)
936+
w.Write([]byte(`<!DOCTYPE html><html><body>` + body + `</body></html>`))
937+
}))
938+
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
939+
server.Close()
940+
if err != nil {
941+
t.Fatalf("%s: unexpected error: %v", name, err)
942+
}
943+
if result != nil && result.Name == name {
944+
t.Errorf("false positive: prose mentioning %s detected as %s (%.2f)", name, name, result.Confidence)
945+
}
946+
}
947+
}
948+
949+
// the bare body words let a single prose mention outscore the site's real,
950+
// multi-marker framework: MatchSignatures normalizes by total signature weight,
951+
// so a lone signature always resolved to a full 1.0, and the single-argmax
952+
// reducer then swapped the true framework for the spurious one.
953+
func TestDetectFramework_ProseDoesNotSuppressRealFramework(t *testing.T) {
954+
for _, mention := range []string{"strapi", "cakephp"} {
955+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
956+
w.WriteHeader(http.StatusOK)
957+
w.Write([]byte(`<!DOCTYPE html><html><head>` +
958+
`<link rel="stylesheet" href="/wp-content/themes/x/style.css">` +
959+
`<script src="/wp-includes/js/jquery.js"></script></head><body>` +
960+
`<p>we compare ` + mention + ` against other platforms.</p></body></html>`))
961+
}))
962+
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
963+
server.Close()
964+
if err != nil {
965+
t.Fatalf("%s: unexpected error: %v", mention, err)
966+
}
967+
if result == nil {
968+
t.Fatalf("%s: expected WordPress, got nil", mention)
969+
}
970+
if result.Name != "WordPress" {
971+
t.Errorf("prose mention of %q suppressed the real framework: got %q (%.4f), want WordPress",
972+
mention, result.Name, result.Confidence)
973+
}
974+
}
975+
}
976+
925977
func TestDetectFramework_Ember(t *testing.T) {
926978
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
927979
w.WriteHeader(http.StatusOK)

internal/scan/frameworks/detectors/backend.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,13 @@ type strapiDetector struct{}
356356
func (d *strapiDetector) Name() string { return "Strapi" }
357357

358358
func (d *strapiDetector) Signatures() []fw.Signature {
359+
// the bare "strapi" body word matched any page that merely named the
360+
// framework (a blog post, a listicle), and as a lone signature it always
361+
// normalized to a full 1.0 score, suppressing the site's real framework.
362+
// key on the structural marker instead: Strapi's default poweredBy
363+
// middleware sets "X-Powered-By: Strapi <strapi.io>" on every response.
359364
return []fw.Signature{
360-
{Pattern: "strapi", Weight: 0.4},
365+
{Pattern: "Strapi", Weight: 0.4, HeaderOnly: true},
361366
}
362367
}
363368

@@ -402,8 +407,10 @@ type cakephpDetector struct{}
402407
func (d *cakephpDetector) Name() string { return "CakePHP" }
403408

404409
func (d *cakephpDetector) Signatures() []fw.Signature {
410+
// drop the bare "cakephp" body word: it matched prose that merely named
411+
// the framework. the default CAKEPHP session cookie is the structural
412+
// marker, matched header-only.
405413
return []fw.Signature{
406-
{Pattern: "cakephp", Weight: 0.4},
407414
{Pattern: "CAKEPHP", Weight: 0.4, HeaderOnly: true},
408415
}
409416
}

0 commit comments

Comments
 (0)