diff options
Diffstat (limited to 'internal/processor')
| -rw-r--r-- | internal/processor/markdown.go | 23 | ||||
| -rw-r--r-- | internal/processor/markdown_test.go | 60 |
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:  // 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(``, 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(``, 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 { |
