Skip to content

Commit cf06f10

Browse files
authored
feat: Implement path-based and pattern-based filter rules (#165)
* feat: Implement path-based and pattern-based filter rules Add advanced matching capabilities for file transfer filtering: New Matchers: - MultiExtensionMatcher: Match multiple file extensions with case sensitivity option - SizeMatcher: Match files by size (min/max/between) - AllMatcher: AND logic for combining matchers - CompositeMatcher: Unified AND/OR/NOT composite matching Enhanced Configuration: - extensions: Match by file extensions ["exe", "bat", "ps1"] - directory: Match by path component (e.g., ".git") - min_size/max_size: File size filtering - composite: Complex AND/OR/NOT rules with nested matchers SizeAwareFilter Trait: - check_with_size() for size-based filtering decisions - check_with_size_dest() for rename/copy operations with size New Configuration Types: - CompositeRuleConfig: Define composite rules - CompositeLogicType: and/or/not logic - MatcherConfig: Individual matcher in composite rules Example YAML configuration: ```yaml filter: enabled: true rules: - name: "block-executables" extensions: [exe, sh, bat] action: deny - name: "protect-env" composite: type: and matchers: - pattern: "*.env" - not: path_prefix: /home/ action: deny ``` Closes #139 * fix(security): Add path normalization defense-in-depth to FilterPolicy Priority: MEDIUM Issue: Path traversal sequences (e.g., /var/../etc/passwd) were not automatically normalized in FilterPolicy::check(), potentially allowing filter bypass if callers did not explicitly normalize paths. Changes: - Add normalize_path() call in FilterPolicy::check() to automatically normalize paths before matching against rules - Add test verifying path traversal protection at policy level - Fix clippy lint: NoOpFilter::default() -> NoOpFilter Review-Iteration: 1 * style: Apply rustfmt formatting * docs: Add comprehensive filter system documentation - Document all matcher types (Glob, Prefix, Extension, Directory, Composite) - Add filter architecture diagram showing request flow - Include composite rule examples (AND, OR, NOT logic) - Document operation and user restrictions - Add security features section (path traversal protection) - Document SizeAwareFilter trait usage - Add complete filter configuration example
1 parent 10ec2b1 commit cf06f10

6 files changed

Lines changed: 1411 additions & 32 deletions

File tree

docs/architecture/server-configuration.md

Lines changed: 270 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,69 @@ scp:
150150
# File transfer filtering
151151
filter:
152152
enabled: false # Default: false
153+
default_action: allow # Default action when no rules match: allow, deny, log
154+
153155
rules:
154-
- pattern: "*.exe"
156+
# Match by glob pattern
157+
- name: "block-exe"
158+
pattern: "*.exe"
155159
action: deny
156-
- path_prefix: "/tmp/"
160+
161+
# Match by path prefix (directory tree)
162+
- name: "log-tmp"
163+
path_prefix: "/tmp/"
157164
action: log
158165

166+
# Match by multiple file extensions
167+
- name: "block-executables"
168+
extensions: ["exe", "bat", "sh", "ps1"]
169+
action: deny
170+
171+
# Match by directory component (anywhere in path)
172+
- name: "block-git"
173+
directory: ".git"
174+
action: deny
175+
176+
# Composite rule with AND logic
177+
- name: "protect-env-outside-home"
178+
composite:
179+
type: and
180+
matchers:
181+
- pattern: "*.env"
182+
- not:
183+
path_prefix: "/home"
184+
action: deny
185+
186+
# Composite rule with OR logic
187+
- name: "block-secrets"
188+
composite:
189+
type: or
190+
matchers:
191+
- pattern: "*.key"
192+
- pattern: "*.pem"
193+
- extensions: ["crt", "p12", "pfx"]
194+
action: deny
195+
196+
# Composite rule with NOT logic (whitelist pattern)
197+
- name: "whitelist-data"
198+
composite:
199+
type: not
200+
matcher:
201+
path_prefix: "/data"
202+
action: deny
203+
204+
# Rule with operation restriction
205+
- name: "readonly-logs"
206+
pattern: "*.log"
207+
action: deny
208+
operations: ["upload", "delete"]
209+
210+
# Rule with user restriction
211+
- name: "admin-only-config"
212+
path_prefix: "/etc"
213+
action: deny
214+
users: ["guest", "readonly"]
215+
159216
# Audit logging configuration
160217
audit:
161218
enabled: false # Default: false
@@ -647,6 +704,217 @@ scp -rp ./data/ user@bssh-server:/storage/backup/
647704

648705
---
649706

707+
## File Transfer Filtering
708+
709+
The bssh-server provides a comprehensive policy-based system for controlling file transfers in SFTP and SCP operations. The filter system allows administrators to allow, deny, or log file operations based on various criteria.
710+
711+
### Filter Architecture
712+
713+
```
714+
Filter Request Flow:
715+
┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐
716+
│ File Operation │ --> │ Normalize Path │ --> │ Match Rules │
717+
│ (SFTP/SCP) │ │ (prevent bypass) │ │ (in order) │
718+
└─────────────────┘ └──────────────────┘ └────────────────┘
719+
720+
v
721+
┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐
722+
│ First Match │ --> │ Apply Action │ --> │ Allow/Deny/Log │
723+
│ Wins │ │ (or default) │ │ │
724+
└─────────────────┘ └──────────────────┘ └────────────────┘
725+
```
726+
727+
### Matcher Types
728+
729+
The filter system supports multiple matcher types that can be combined for flexible rule definitions:
730+
731+
| Matcher | Config Key | Description | Example |
732+
|---------|------------|-------------|---------|
733+
| **Glob** | `pattern` | Shell-style glob patterns | `*.exe`, `secret*` |
734+
| **Prefix** | `path_prefix` | Directory tree matching | `/etc`, `/home/user` |
735+
| **Extension** | `extensions` | Multiple file extensions | `["exe", "bat", "sh"]` |
736+
| **Directory** | `directory` | Component anywhere in path | `.git`, `.ssh` |
737+
| **Composite** | `composite` | AND/OR/NOT logic | See below |
738+
739+
### Glob Pattern Matching
740+
741+
Glob patterns support standard wildcards:
742+
- `*` - matches any sequence of characters
743+
- `?` - matches any single character
744+
- `[abc]` - matches any character in the set
745+
- `[!abc]` - matches any character not in the set
746+
747+
```yaml
748+
rules:
749+
- pattern: "*.key" # All .key files
750+
- pattern: "secret?.txt" # secret1.txt, secretA.txt, etc.
751+
- pattern: "[0-9]*.log" # Log files starting with a digit
752+
```
753+
754+
### Extension Matching
755+
756+
Multi-extension matching is case-insensitive by default:
757+
758+
```yaml
759+
rules:
760+
- name: "block-executables"
761+
extensions: ["exe", "bat", "sh", "ps1", "cmd"]
762+
action: deny
763+
764+
- name: "block-archives"
765+
extensions: ["zip", "tar", "gz", "rar", "7z"]
766+
action: deny
767+
```
768+
769+
### Composite Rules
770+
771+
Composite rules allow combining multiple matchers with logical operators:
772+
773+
**AND Logic** - All matchers must match:
774+
```yaml
775+
- name: "env-outside-home"
776+
composite:
777+
type: and
778+
matchers:
779+
- pattern: "*.env"
780+
- not:
781+
path_prefix: "/home"
782+
action: deny
783+
```
784+
785+
**OR Logic** - Any matcher must match:
786+
```yaml
787+
- name: "sensitive-files"
788+
composite:
789+
type: or
790+
matchers:
791+
- pattern: "*.key"
792+
- pattern: "*.pem"
793+
- pattern: "*.p12"
794+
action: deny
795+
```
796+
797+
**NOT Logic** - Invert the match (whitelist pattern):
798+
```yaml
799+
- name: "whitelist-data-only"
800+
composite:
801+
type: not
802+
matcher:
803+
path_prefix: "/data"
804+
action: deny # Deny everything NOT in /data
805+
```
806+
807+
### Operation and User Restrictions
808+
809+
Rules can be limited to specific operations or users:
810+
811+
```yaml
812+
rules:
813+
# Prevent deletion of log files
814+
- name: "protect-logs"
815+
pattern: "*.log"
816+
action: deny
817+
operations: ["delete"]
818+
819+
# Block uploads of executables for guest users
820+
- name: "guest-no-executables"
821+
extensions: ["exe", "sh", "bat"]
822+
action: deny
823+
operations: ["upload"]
824+
users: ["guest", "anonymous"]
825+
```
826+
827+
**Available Operations:**
828+
- `upload` - File uploads
829+
- `download` - File downloads
830+
- `delete` - File deletion
831+
- `rename` - File rename/move
832+
- `createdir` - Directory creation
833+
- `listdir` - Directory listing
834+
- `stat` - Reading file attributes
835+
- `setstat` - Modifying file attributes
836+
- `symlink` - Creating symbolic links
837+
- `readlink` - Reading symbolic link targets
838+
839+
### Security Features
840+
841+
**Path Traversal Protection:**
842+
All paths are normalized before matching to prevent bypass attempts:
843+
```
844+
/var/../etc/passwd -> /etc/passwd
845+
/home/user/../../etc -> /etc
846+
```
847+
848+
**First Match Wins:**
849+
Rules are evaluated in order. The first matching rule determines the action. If no rules match, the default action (configurable, defaults to `allow`) is used.
850+
851+
### SizeAwareFilter Trait
852+
853+
For size-based filtering (e.g., blocking large uploads), the `SizeAwareFilter` trait provides:
854+
855+
```rust
856+
use bssh::server::filter::{SizeAwareFilter, FilterResult, Operation};
857+
use bssh::server::filter::path::SizeMatcher;
858+
859+
// Create a size matcher for files over 100MB
860+
let large_file_matcher = SizeMatcher::min(100 * 1024 * 1024);
861+
862+
// Check if the given size matches
863+
assert!(large_file_matcher.matches_size(200 * 1024 * 1024)); // 200MB matches
864+
assert!(!large_file_matcher.matches_size(50 * 1024 * 1024)); // 50MB doesn't match
865+
```
866+
867+
**Note:** Size-based filtering in configuration requires implementation integration with the actual file transfer handlers.
868+
869+
### Complete Filter Configuration Example
870+
871+
```yaml
872+
filter:
873+
enabled: true
874+
default_action: allow
875+
876+
rules:
877+
# Block dangerous executables
878+
- name: "block-executables"
879+
extensions: ["exe", "bat", "sh", "ps1", "cmd", "com"]
880+
action: deny
881+
882+
# Block private keys and certificates
883+
- name: "block-secrets"
884+
composite:
885+
type: or
886+
matchers:
887+
- pattern: "*.key"
888+
- pattern: "*.pem"
889+
- pattern: "*.p12"
890+
- pattern: "id_rsa*"
891+
- pattern: "id_ed25519*"
892+
action: deny
893+
894+
# Block hidden directories
895+
- name: "block-hidden"
896+
directory: ".git"
897+
action: deny
898+
899+
- name: "block-ssh-config"
900+
directory: ".ssh"
901+
action: deny
902+
903+
# Log access to configuration files
904+
- name: "log-config-access"
905+
path_prefix: "/etc"
906+
action: log
907+
908+
# Restrict guests to read-only access in /data
909+
- name: "guest-read-only"
910+
path_prefix: "/data"
911+
operations: ["upload", "delete", "rename", "createdir", "setstat"]
912+
users: ["guest"]
913+
action: deny
914+
```
915+
916+
---
917+
650918
## Session Management
651919

652920
The server implements comprehensive session management with per-user limits, idle timeout detection, and session tracking.

src/server/config/types.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ pub struct FilterConfig {
289289
}
290290

291291
/// A single file transfer filter rule.
292-
#[derive(Debug, Clone, Deserialize, Serialize)]
292+
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
293293
pub struct FilterRule {
294294
/// Rule name (for logging and debugging).
295295
///
@@ -309,6 +309,37 @@ pub struct FilterRule {
309309
#[serde(default)]
310310
pub path_prefix: Option<String>,
311311

312+
/// File extensions to match.
313+
///
314+
/// Example: ["exe", "sh", "bat"] blocks executable files
315+
#[serde(default)]
316+
pub extensions: Option<Vec<String>>,
317+
318+
/// Directory component to match.
319+
///
320+
/// Matches if any path component equals this value.
321+
/// Example: ".git" matches /project/.git/config
322+
#[serde(default)]
323+
pub directory: Option<String>,
324+
325+
/// Minimum file size in bytes.
326+
///
327+
/// Files smaller than this size will not match.
328+
#[serde(default)]
329+
pub min_size: Option<u64>,
330+
331+
/// Maximum file size in bytes.
332+
///
333+
/// Files larger than this size will not match.
334+
#[serde(default)]
335+
pub max_size: Option<u64>,
336+
337+
/// Composite rule configuration.
338+
///
339+
/// Allows combining multiple matchers with AND/OR/NOT logic.
340+
#[serde(default)]
341+
pub composite: Option<CompositeRuleConfig>,
342+
312343
/// Action to take when rule matches.
313344
pub action: FilterAction,
314345

@@ -326,6 +357,59 @@ pub struct FilterRule {
326357
pub users: Option<Vec<String>>,
327358
}
328359

360+
/// Configuration for composite filter rules.
361+
#[derive(Debug, Clone, Deserialize, Serialize)]
362+
pub struct CompositeRuleConfig {
363+
/// Type of composite logic: "and", "or", or "not"
364+
#[serde(rename = "type")]
365+
pub logic_type: CompositeLogicType,
366+
367+
/// List of matchers for AND/OR logic.
368+
#[serde(default)]
369+
pub matchers: Vec<MatcherConfig>,
370+
371+
/// Single matcher for NOT logic.
372+
#[serde(default)]
373+
pub matcher: Option<Box<MatcherConfig>>,
374+
}
375+
376+
/// Type of composite logic.
377+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
378+
#[serde(rename_all = "lowercase")]
379+
pub enum CompositeLogicType {
380+
/// All matchers must match
381+
And,
382+
/// Any matcher must match
383+
Or,
384+
/// Invert the matcher result
385+
Not,
386+
}
387+
388+
/// Configuration for a single matcher within a composite rule.
389+
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
390+
#[serde(default)]
391+
pub struct MatcherConfig {
392+
/// Glob pattern
393+
#[serde(default)]
394+
pub pattern: Option<String>,
395+
396+
/// Path prefix
397+
#[serde(default)]
398+
pub path_prefix: Option<String>,
399+
400+
/// File extensions
401+
#[serde(default)]
402+
pub extensions: Option<Vec<String>>,
403+
404+
/// Directory component
405+
#[serde(default)]
406+
pub directory: Option<String>,
407+
408+
/// Nested NOT matcher
409+
#[serde(default)]
410+
pub not: Option<Box<MatcherConfig>>,
411+
}
412+
329413
/// Action to take when a filter rule matches.
330414
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
331415
#[serde(rename_all = "lowercase")]

0 commit comments

Comments
 (0)