summaryrefslogtreecommitdiff
path: root/internal/processor
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processor')
-rw-r--r--internal/processor/markdown.go23
-rw-r--r--internal/processor/markdown_test.go60
2 files changed, 82 insertions, 1 deletions
diff --git a/internal/processor/markdown.go b/internal/processor/markdown.go
index e09cf59..c258e4c 100644
--- a/internal/processor/markdown.go
+++ b/internal/processor/markdown.go
@@ -13,6 +13,18 @@ import (
"github.com/yuin/goldmark/renderer/html"
)
+// isSimpleImageRef returns true for a filename-only reference (e.g.
+// "img.png") that is safe to treat as a flat local file in the same
+// directory as the markdown source. It rejects subdirectories, absolute
+// paths, dot-slash prefixes, and parent-directory traversal so stat and
+// copy targets stay within the source directory.
+func isSimpleImageRef(ref string) bool {
+ if strings.Contains(ref, "..") {
+ return false
+ }
+ return filepath.Base(ref) == ref
+}
+
// imageRefPattern matches Markdown image syntax: ![alt](filename)
// We use it to discover local asset references that must be copied.
var imageRefPattern = regexp.MustCompile(`!\[[^\]]*\]\(([^)]+)\)`)
@@ -62,9 +74,18 @@ func findLocalImages(mdContent, sourceDir string) []string {
continue
}
+ // Reject references that traverse directories or contain path
+ // separators; only flat filenames next to the markdown are
+ // supported. This prevents scans from succeeding on a file
+ // deep in a subdirectory and then failing copy because the
+ // basename is looked up in the wrong directory.
+ if !isSimpleImageRef(ref) {
+ continue
+ }
+
candidate := filepath.Join(sourceDir, ref)
if _, err := os.Stat(candidate); err == nil {
- locals = append(locals, filepath.Base(ref))
+ locals = append(locals, ref)
}
}
diff --git a/internal/processor/markdown_test.go b/internal/processor/markdown_test.go
index 2445b53..c53874d 100644
--- a/internal/processor/markdown_test.go
+++ b/internal/processor/markdown_test.go
@@ -9,6 +9,33 @@ import (
"codeberg.org/snonux/snonux/internal/config"
)
+func TestIsSimpleImageRef(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ ref string
+ want bool
+ }{
+ {"img.png", true},
+ {"a.png", true},
+ {"sub/img.png", false},
+ {"a/b/c.png", false},
+ {"../img.png", false},
+ {"img/../other.png", false},
+ {"/etc/passwd", false},
+ }
+
+ for _, c := range cases {
+ t.Run(c.ref, func(t *testing.T) {
+ t.Parallel()
+ got := isSimpleImageRef(c.ref)
+ if got != c.want {
+ t.Fatalf("isSimpleImageRef(%q) = %v; want %v", c.ref, got, c.want)
+ }
+ })
+ }
+}
+
func TestFindLocalImages(t *testing.T) {
t.Parallel()
@@ -42,6 +69,38 @@ func TestFindLocalImages(t *testing.T) {
}
})
+ t.Run("subdir ref ignored even if file exists", func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "sub", "photo.png"), []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ got := findLocalImages(`![](sub/photo.png)`, dir)
+ if len(got) != 0 {
+ t.Fatalf("expected no locals for subdir ref, got %v", got)
+ }
+ })
+
+ t.Run("parent traversal ref ignored even if file exists", func(t *testing.T) {
+ t.Parallel()
+ // Create a layout where ../photo.png from dir would resolve to a real file.
+ base := t.TempDir()
+ dir := filepath.Join(base, "inbox")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(base, "photo.png"), []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ got := findLocalImages(`![](../photo.png)`, dir)
+ if len(got) != 0 {
+ t.Fatalf("expected no locals for traversal ref, got %v", got)
+ }
+ })
+
tests := []struct {
name string
md string
@@ -62,6 +121,7 @@ func TestFindLocalImages(t *testing.T) {
want: []string{"z.gif"},
wantLen: 1,
},
+
}
for _, tt := range tests {