- 43 tests across SlugGuard, ReplContext navigation, NotesCache.TimeAgo - SlugGuard moved to NozCli.Core.Security (shared between REPL and tests) - Bug fix: FoldersInView() now returns only actual sub-folders, not note segments (previously cd tab-completion suggested note names as navigable folders) - .forgejo/workflows/ci.yml: build + test on every push/PR
34 lines
1.4 KiB
C#
34 lines
1.4 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace NozCli.Core.Security;
|
|
|
|
/// Validates slugs before they are sent to the server or written to disk.
|
|
public static partial class SlugGuard
|
|
{
|
|
// Valid slug: lowercase alphanumeric + hyphens, optionally separated by /
|
|
// Examples: "hydroponik", "biosysteme/hydroponik", "a/b/c"
|
|
[GeneratedRegex(@"^[a-z0-9][a-z0-9\-]*(?:/[a-z0-9][a-z0-9\-]*)*$")]
|
|
private static partial Regex ValidSlugRegex();
|
|
|
|
/// Returns true if the slug is safe to use (no path traversal, valid chars).
|
|
public static bool IsValid(string slug) =>
|
|
!string.IsNullOrWhiteSpace(slug) &&
|
|
!slug.Contains("..") &&
|
|
!slug.Contains("//") &&
|
|
ValidSlugRegex().IsMatch(slug);
|
|
|
|
/// Resolves a slug to a file path inside baseDir.
|
|
/// Returns null if the resulting path would escape baseDir (path traversal).
|
|
public static string? ToSafePath(string baseDir, string slug, string extension = ".md")
|
|
{
|
|
var rel = slug.Replace('/', Path.DirectorySeparatorChar) + extension;
|
|
var fullPath = Path.GetFullPath(Path.Combine(baseDir, rel));
|
|
var baseFull = Path.GetFullPath(baseDir);
|
|
|
|
// Ensure the resolved path stays strictly inside baseDir
|
|
return fullPath.StartsWith(baseFull + Path.DirectorySeparatorChar,
|
|
StringComparison.OrdinalIgnoreCase)
|
|
? fullPath
|
|
: null;
|
|
}
|
|
}
|