Skip to content

Commit 56a7bdb

Browse files
committed
fix(modules): anchor the backup-files html guard and narrow it
the guard was unanchored and covered head|title|body, so it matched those tags anywhere in the response. a cms dump is full of markup: wp_posts.post_content holds rendered html and page builders stash whole documents in serialized postmeta, so the negative fired on the exact dumps this module exists to catch and the finding went silent. anchor at the body start and keep only the two document-opening tags, matching the maven-settings/gradle-properties/nuget-config convention. a soft-404 shell still opens as an html document and is still suppressed. the module shipped with no test; add one covering a wordpress dump with html in post_content, a plain .env backup, the soft-404 shell and a 404.
1 parent 8e2478e commit 56a7bdb

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package modules_test
14+
15+
import (
16+
"context"
17+
"net/http"
18+
"net/http/httptest"
19+
"testing"
20+
"time"
21+
22+
"github.com/vmfunc/sif/internal/modules"
23+
)
24+
25+
// runBackupFilesModule runs the backup-files module end to end against a server
26+
// that returns the same status and body for every path it requests.
27+
func runBackupFilesModule(t *testing.T, status int, body string) *modules.Result {
28+
t.Helper()
29+
def, err := modules.ParseYAMLModule("../../modules/recon/backup-files.yaml")
30+
if err != nil {
31+
t.Fatalf("parse: %v", err)
32+
}
33+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34+
w.WriteHeader(status)
35+
_, _ = w.Write([]byte(body))
36+
}))
37+
defer srv.Close()
38+
39+
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
40+
Timeout: 5 * time.Second,
41+
Threads: 2,
42+
})
43+
if err != nil {
44+
t.Fatalf("execute: %v", err)
45+
}
46+
return res
47+
}
48+
49+
func TestBackupFilesExposureModule(t *testing.T) {
50+
// a cms dump carries markup in post_content and postmeta, which is what the
51+
// html guard has to tolerate.
52+
wordpressDump := "-- MySQL dump 10.13\n" +
53+
"CREATE TABLE `wp_posts` (\n `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT\n);\n" +
54+
"INSERT INTO `wp_posts` VALUES (1,'<title>Hello world</title>'," +
55+
"'<body class=\"home\"><h1>Welcome</h1></body>');\n" +
56+
"INSERT INTO `wp_postmeta` VALUES (7,1,'_elementor_data'," +
57+
"'a:1:{s:4:\"html\";s:32:\"<!doctype html><html>saved</html>\";}');\n"
58+
59+
// the soft-404 the guard was added for: the site's own shell, served 200.
60+
softFourOhFour := "<!DOCTYPE html>\n<html><head><title>Not found</title></head>\n" +
61+
"<body><p>Set your SECRET_KEY and APP_KEY before deploying.</p></body></html>\n"
62+
63+
// a plain .env backup, no markup anywhere.
64+
envBackup := "APP_KEY=base64:Zm9vYmFy\nDB_PASSWORD=s3cr3t\n"
65+
66+
t.Run("a wordpress sql dump carrying html in post content is flagged", func(t *testing.T) {
67+
if res := runBackupFilesModule(t, 200, wordpressDump); len(res.Findings) == 0 {
68+
t.Error("a real cms dump was suppressed by the html guard, this is the exact file the module is for")
69+
}
70+
})
71+
72+
t.Run("a plain env backup is flagged", func(t *testing.T) {
73+
if res := runBackupFilesModule(t, 200, envBackup); len(res.Findings) == 0 {
74+
t.Error("expected a finding for a plain .env backup")
75+
}
76+
})
77+
78+
t.Run("an html soft-404 shell mentioning the tokens is not flagged", func(t *testing.T) {
79+
if res := runBackupFilesModule(t, 200, softFourOhFour); len(res.Findings) > 0 {
80+
t.Errorf("html soft-404 shell should be suppressed, got %d findings", len(res.Findings))
81+
}
82+
})
83+
84+
t.Run("a 404 is not a leak", func(t *testing.T) {
85+
if res := runBackupFilesModule(t, 404, envBackup); len(res.Findings) > 0 {
86+
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
87+
}
88+
})
89+
}

modules/recon/backup-files.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,15 @@ http:
4545
condition: or
4646

4747
# a 200 soft-404 that serves the site's html shell trips the generic tokens
48-
# above (SECRET_KEY/APP_KEY appear in docs prose); suppress any response that
49-
# opens like an html document. real dumps are plain text and carry no markup.
48+
# above (SECRET_KEY/APP_KEY appear in docs prose); suppress a response that
49+
# opens as an html document.
50+
#
51+
# anchored, and only on the two document-opening tags. a cms dump is full of
52+
# markup: wp_posts.post_content holds rendered html and page builders stash
53+
# whole documents in serialized postmeta, so an unanchored guard covering
54+
# head/title/body silently drops the dumps this module exists to catch.
5055
- type: regex
5156
part: body
5257
negative: true
5358
regex:
54-
- '(?i)<(!doctype|html|head|title|body)[ >]'
59+
- '(?i)^\s*<(!doctype|html)[ >]'

0 commit comments

Comments
 (0)