Skip to content

Commit db79f9e

Browse files
authored
Merge branch 'develop' into dependabot/github_actions/github/codeql-action-4.35.5
2 parents c41c7b5 + 247fcce commit db79f9e

8 files changed

Lines changed: 703 additions & 45 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Please refer to the community [README](https://github.com/oscal-compass/communit
113113

114114
Our project welcomes external contributions. Please consult [contributing](https://oscal-compass.github.io/compliance-trestle/latest/contributing/mkdocs_contributing/) to get started.
115115

116+
## Security
117+
118+
For information about security features, best practices, and how to report security vulnerabilities, please see our [Security Policy](SECURITY.md).
119+
116120
## Code of Conduct
117121

118122
Participation in the OSCAL Compass community is governed by the [Code of Conduct](https://github.com/oscal-compass/community/blob/main/CODE_OF_CONDUCT.md).

SECURITY.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Security Policy
2+
3+
## Reporting Security Vulnerabilities
4+
5+
For information about how to report security vulnerabilities, please see the [OSCAL Compass Community Security Policy](https://github.com/oscal-compass/community/blob/main/SECURITY.md).
6+
7+
## Security Features
8+
9+
### SSRF (Server-Side Request Forgery) Protection
10+
11+
Compliance-trestle implements comprehensive SSRF protection when fetching remote OSCAL content via HTTPS or SFTP. This protection uses a **two-tier defense system** to prevent malicious actors from exploiting the fetching mechanism to access internal resources or cloud metadata endpoints.
12+
13+
#### Tier 1: Always Blocked (Zero Tolerance)
14+
15+
The following address ranges and endpoints are **always blocked** regardless of configuration, as they have zero legitimate use for OSCAL content fetching:
16+
17+
- **Loopback addresses**: `127.0.0.0/8` (IPv4), `::1/128` (IPv6)
18+
- **Link-local addresses**: `169.254.0.0/16` (IPv4), `fe80::/10` (IPv6)
19+
- **Cloud metadata endpoints**:
20+
- `169.254.169.254` (AWS, Azure, GCP)
21+
- `metadata.google.internal` (GCP)
22+
- `metadata.azure.com` (Azure alternative)
23+
- `100.100.100.200` (Alibaba Cloud)
24+
25+
These ranges are blocked to prevent:
26+
27+
- Access to localhost services
28+
- Exploitation of cloud metadata endpoints to steal credentials
29+
- Access to link-local services
30+
31+
#### Tier 2: Optionally Blocked (Configurable)
32+
33+
RFC 1918 private IP ranges are **allowed by default** to support legitimate use cases such as private GitLab instances or internal OSCAL repositories:
34+
35+
- `10.0.0.0/8`
36+
- `172.16.0.0/12`
37+
- `192.168.0.0/16`
38+
- `fc00::/7` (IPv6 unique local)
39+
40+
**To block private IP ranges**, set the environment variable:
41+
42+
```bash
43+
export TRESTLE_BLOCK_PRIVATE_IPS=true
44+
```
45+
46+
When private IPs are allowed (default), trestle logs a warning when accessing them to maintain visibility.
47+
48+
#### Domain Allowlist (Optional)
49+
50+
For additional security, you can restrict fetching to specific domains by configuring an allowed domains list. When configured, only URLs from the specified domains will be permitted.
51+
52+
### Path Traversal Protection
53+
54+
Trestle implements multiple layers of path traversal protection:
55+
56+
1. **URL Path Validation**: Blocks `..` sequences in URL paths to prevent directory traversal
57+
1. **Cache Path Validation**: Ensures cached files remain within the designated cache directory
58+
1. **Workspace Boundary Enforcement**: Validates that local file operations stay within the trestle workspace
59+
1. **Sensitive File Protection**: Blocks access to sensitive system files even when outside-workspace access is allowed:
60+
- `/etc/passwd`, `/etc/shadow`, `/etc/group`, `/etc/sudoers`
61+
- SSH keys (`.ssh/`)
62+
- Cloud credentials (`.aws/`, `.docker/`, `.kube/`)
63+
- System logs (`/var/log/`)
64+
- Database files (`/var/lib/mysql/`)
65+
- Windows system files (`C:\Windows\System32\`, credentials)
66+
- Process information (`/proc/self/environ`)
67+
68+
### Scheme Restrictions
69+
70+
Only HTTPS and SFTP schemes are allowed for remote URLs. HTTP, FTP, and other protocols are rejected to ensure encrypted transport.
71+
72+
### Port Restrictions
73+
74+
By default, only standard ports are allowed:
75+
76+
- HTTPS: port 443
77+
- SFTP: port 22
78+
79+
Non-standard ports are blocked unless explicitly configured.
80+
81+
## Security Best Practices
82+
83+
When using compliance-trestle to fetch remote OSCAL content:
84+
85+
1. **Use HTTPS URLs** from trusted sources
86+
1. **Enable private IP blocking** (`TRESTLE_BLOCK_PRIVATE_IPS=true`) in production environments unless you specifically need to access private repositories
87+
1. **Configure domain allowlists** when fetching from a known set of trusted domains
88+
1. **Monitor logs** for warnings about private IP access
89+
1. **Keep trestle updated** to receive the latest security fixes
90+
1. **Review fetched content** before using it in production compliance workflows
91+
92+
## Security Testing
93+
94+
The SSRF and path traversal protections are comprehensively tested with 100% code coverage. Tests include:
95+
96+
- Blocking of all Tier 1 addresses and endpoints
97+
- Configurable blocking of Tier 2 private ranges
98+
- Path traversal attack vectors
99+
- Sensitive file access attempts
100+
- Real-world attack scenarios from security advisories
101+
102+
## Version History
103+
104+
- **v4.x**: Introduced two-tier SSRF protection system (GHSA-w76h-q7c6-jpjp fix)
105+
- **v3.x and earlier**: Limited SSRF protection (vulnerable)
106+
107+
## References
108+
109+
- [GHSA-w76h-q7c6-jpjp](https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-w76h-q7c6-jpjp) - SSRF vulnerability advisory
110+
- [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)
111+
- [CWE-918: Server-Side Request Forgery (SSRF)](https://cwe.mitre.org/data/definitions/918.html)

tests/trestle/core/commands/author/jinja_cmd_test.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
import os
1717
import pathlib
1818
import shutil
19+
from types import SimpleNamespace
20+
21+
import pytest
1922

2023
from _pytest.monkeypatch import MonkeyPatch
2124

2225
from tests.test_utils import execute_command_and_assert, setup_for_ssp
2326

24-
from trestle.core.commands.author.jinja import _number_captions
27+
from trestle.common.err import TrestleError
28+
from trestle.core.commands.author.jinja import JinjaCmd, _number_captions
2529
from trestle.core.commands.author.ssp import SSPGenerate
2630
from trestle.core.markdown.docs_markdown_node import DocsMarkdownNode
2731

@@ -295,3 +299,109 @@ def test_jinja_with_template_only(
295299
node1 = tree.get_node_for_key('# A')
296300
node2 = tree.get_node_for_key('# C')
297301
assert node1.subnodes[0].key == node2.subnodes[0].key
302+
303+
304+
def test_jinja_path_traversal_protection(
305+
testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch
306+
) -> None:
307+
"""Test that path traversal attacks are blocked in jinja command."""
308+
from trestle.core.remote.security import PathSecurityValidator
309+
310+
# Test path validation directly to ensure 100% coverage of the validation code
311+
# Test 1: Path traversal with ../ should fail
312+
with pytest.raises(TrestleError) as exc_info:
313+
output_file = tmp_trestle_dir / '../../../etc/passwd'
314+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
315+
assert 'Security violation' in str(exc_info.value)
316+
assert 'Path traversal blocked' in str(exc_info.value)
317+
318+
# Test 2: Path traversal with multiple ../ should fail
319+
with pytest.raises(TrestleError) as exc_info:
320+
output_file = tmp_trestle_dir / 'subdir/../../poc.txt'
321+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
322+
assert 'Security violation' in str(exc_info.value)
323+
324+
# Test 3: Absolute path should fail
325+
with pytest.raises(TrestleError) as exc_info:
326+
output_file = pathlib.Path('/tmp/attack.md')
327+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
328+
assert 'Security violation' in str(exc_info.value)
329+
330+
# Test 4: Complex traversal should fail
331+
with pytest.raises(TrestleError) as exc_info:
332+
output_file = tmp_trestle_dir / 'a/b/c/../../../../etc/passwd'
333+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
334+
assert 'Security violation' in str(exc_info.value)
335+
336+
# Test 5: Valid relative path should succeed
337+
output_file = tmp_trestle_dir / 'output/valid.md'
338+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir) # Should not raise
339+
340+
341+
def test_jinja_docs_profile_path_traversal_protection(tmp_trestle_dir: pathlib.Path) -> None:
342+
"""Test that path traversal attacks are blocked in jinja docs-profile mode."""
343+
from trestle.core.remote.security import PathSecurityValidator
344+
345+
# Test validation for multi-file output paths
346+
# Test 1: Path traversal in output directory should fail
347+
with pytest.raises(TrestleError) as exc_info:
348+
output_file = tmp_trestle_dir / '../../../etc/ac-1.md'
349+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
350+
assert 'Security violation' in str(exc_info.value)
351+
assert 'Path traversal blocked' in str(exc_info.value)
352+
353+
# Test 2: Complex path traversal should fail
354+
with pytest.raises(TrestleError) as exc_info:
355+
output_file = tmp_trestle_dir / 'controls/../../tmp/ac-1.md'
356+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
357+
assert 'Security violation' in str(exc_info.value)
358+
359+
# Test 3: Directory creation path traversal should fail
360+
with pytest.raises(TrestleError) as exc_info:
361+
group_dir = tmp_trestle_dir / '../../../etc/malicious'
362+
PathSecurityValidator.validate_local_path(group_dir, tmp_trestle_dir)
363+
assert 'Security violation' in str(exc_info.value)
364+
365+
# Test 4: Valid relative path should succeed
366+
output_file = tmp_trestle_dir / 'controls_output/ac/ac-1.md'
367+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir) # Should not raise
368+
369+
370+
def test_render_template_does_not_recursively_evaluate_untrusted_data(tmp_path: pathlib.Path) -> None:
371+
"""Test that rendered attacker-controlled data is not re-evaluated as Jinja."""
372+
template_path = tmp_path / 'template.j2'
373+
template_path.write_text('Title: {{ ssp.metadata.title }}', encoding='utf-8')
374+
375+
jinja_env = JinjaCmd._create_jinja_environment(tmp_path)
376+
template = jinja_env.get_template(template_path.name)
377+
378+
lut = {
379+
'ssp': SimpleNamespace(
380+
metadata=SimpleNamespace(title="{{ namespace.__init__.__globals__.os.system('touch poc.txt') }}")
381+
)
382+
}
383+
384+
output = JinjaCmd.render_template(template, lut, tmp_path)
385+
386+
assert output.startswith('Title: {{ namespace.__init__.__globals__.os.system(')
387+
assert 'touch poc.txt' in output
388+
assert '{{' in output
389+
assert '}}' in output
390+
assert '&' in output
391+
assert not (tmp_path / 'poc.txt').exists()
392+
393+
394+
def test_render_template_supports_trusted_include(tmp_path: pathlib.Path) -> None:
395+
"""Test that trusted template includes continue to work."""
396+
include_path = tmp_path / 'partial.j2'
397+
include_path.write_text('World', encoding='utf-8')
398+
399+
template_path = tmp_path / 'template.j2'
400+
template_path.write_text("Hello {% include 'partial.j2' %}", encoding='utf-8')
401+
402+
jinja_env = JinjaCmd._create_jinja_environment(tmp_path)
403+
template = jinja_env.get_template(template_path.name)
404+
405+
output = JinjaCmd.render_template(template, {}, tmp_path)
406+
407+
assert output == 'Hello World'

0 commit comments

Comments
 (0)