Logger System 0.1.3
High-performance C++20 thread-safe logging system with asynchronous capabilities
Loading...
Searching...
No Matches
path_validator.h
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
11#pragma once
12
14#include <filesystem>
15#include <algorithm>
16#include <cctype>
17
19
31public:
36 explicit path_validator(std::filesystem::path allowed_base)
37 : allowed_base_(std::move(allowed_base)) {
38
39 // Convert to canonical path if it exists
40 try {
41 if (std::filesystem::exists(allowed_base_)) {
42 allowed_base_ = std::filesystem::canonical(allowed_base_);
43 } else {
44 // Use weakly_canonical for non-existent paths
45 allowed_base_ = std::filesystem::weakly_canonical(allowed_base_);
46 }
47 } catch (const std::filesystem::filesystem_error&) {
48 // If canonicalization fails, use as-is
49 // Validation will catch issues later
50 }
51 }
52
61 const std::filesystem::path& path,
62 bool allow_symlinks = false,
63 bool strict_filename = true
64 ) const {
65 try {
66 // 1. Convert to absolute path and resolve relative components
67 std::filesystem::path canonical;
68 if (std::filesystem::exists(path)) {
69 canonical = std::filesystem::canonical(path);
70 } else {
71 canonical = std::filesystem::weakly_canonical(path);
72 }
73
74 // 2. Check for symbolic links (if not allowed)
75 if (!allow_symlinks && std::filesystem::exists(path)) {
76 if (std::filesystem::is_symlink(path)) {
79 "Symbolic links are not allowed for security reasons"
80 );
81 }
82 }
83
84 // 3. Verify path is within allowed base directory
85 auto [base_end, path_end] = std::mismatch(
86 allowed_base_.begin(), allowed_base_.end(),
87 canonical.begin(), canonical.end()
88 );
89
90 if (base_end != allowed_base_.end()) {
93 "Path must be within allowed directory: " + allowed_base_.string()
94 );
95 }
96
97 // 4. Validate filename (if strict mode enabled)
98 if (strict_filename && path.has_filename()) {
99 auto filename = path.filename().string();
100
101 if (!is_safe_filename(filename)) {
104 "Filename contains invalid or potentially dangerous characters"
105 );
106 }
107 }
108
109 return common::ok();
110
111 } catch (const std::filesystem::filesystem_error& e) {
114 std::string("Path validation failed: ") + e.what()
115 );
116 }
117 }
118
122 const std::filesystem::path& allowed_base() const {
123 return allowed_base_;
124 }
125
137 static bool is_safe_filename(const std::string& name) {
138 // Empty or special names are not allowed
139 if (name.empty() || name == "." || name == "..") {
140 return false;
141 }
142
143 // Check each character
144 for (char c : name) {
145 if (!std::isalnum(static_cast<unsigned char>(c)) &&
146 c != '-' && c != '_' && c != '.') {
147 return false;
148 }
149 }
150
151 return true;
152 }
153
160 static std::string sanitize_filename(
161 const std::string& name,
162 char replacement = '_'
163 ) {
164 // Handle special cases
165 if (name.empty()) {
166 return "unnamed";
167 }
168 if (name == ".") {
169 return std::string(1, replacement) + name;
170 }
171 if (name == "..") {
172 return std::string(1, replacement) + '.';
173 }
174
175 std::string result;
176 result.reserve(name.size());
177
178 for (char c : name) {
179 if (std::isalnum(static_cast<unsigned char>(c)) ||
180 c == '-' || c == '_' || c == '.') {
181 result += c;
182 } else {
183 result += replacement;
184 }
185 }
186
187 return result;
188 }
189
197 const std::filesystem::path& base,
198 const std::filesystem::path& relative
199 ) {
200 try {
201 // Ensure relative path doesn't contain absolute components
202 if (relative.is_absolute()) {
205 "Cannot join with absolute path"};
206 }
207
208 // Join paths
209 auto joined = base / relative;
210
211 // Validate the result
212 path_validator validator(base);
213 auto validation = validator.validate(joined);
214
215 if (validation.is_err()) {
217 get_logger_error_code(validation),
218 get_logger_error_message(validation)};
219 }
220
221 return result<std::filesystem::path>(joined);
222
223 } catch (const std::filesystem::filesystem_error& e) {
226 std::string("Path join failed: ") + e.what()};
227 }
228 }
229
230private:
231 std::filesystem::path allowed_base_;
232};
233
234} // namespace kcenon::logger::security
Validates file paths to prevent security vulnerabilities.
static bool is_safe_filename(const std::string &name)
Check if a filename is safe (contains only allowed characters)
static std::string sanitize_filename(const std::string &name, char replacement='_')
Sanitize a filename by removing/replacing invalid characters.
common::VoidResult validate(const std::filesystem::path &path, bool allow_symlinks=false, bool strict_filename=true) const
Validate a file path.
const std::filesystem::path & allowed_base() const
Get the allowed base directory.
static result< std::filesystem::path > safe_join(const std::filesystem::path &base, const std::filesystem::path &relative)
Create a safe path by joining base and relative path.
path_validator(std::filesystem::path allowed_base)
Construct with allowed base directory.
Error codes specific to the logger system.
VoidResult ok()
common::VoidResult make_logger_void_result(logger_error_code code, const std::string &message="")
std::string get_logger_error_message(const common::VoidResult &result)
Get error message from a VoidResult.
logger_error_code get_logger_error_code(const common::VoidResult &result)