Skip to content

Commit b7bb487

Browse files
authored
Merge pull request from GHSA-jh85-wwv9-24hv
* Add `restrict_base_path` and make it the default New option to restrict snippets to be actual children of the base path for a more sane default. * Update grammar
1 parent a8fb966 commit b7bb487

File tree

6 files changed

+88
-9
lines changed

6 files changed

+88
-9
lines changed

docs/src/markdown/about/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 10.0
4+
5+
- **Break**: Snippets: snippets will restrict snippets to ensure they are under the `base_path` preventing snippets
6+
relative to the `base_path` but not explicitly under it. `restrict_base_path` can be set to `False` for legacy
7+
behavior.
8+
39
## 9.11
410

511
- **NEW**: Emoji: Update to new CDN and use Twemoji 14.1.2.

docs/src/markdown/extensions/snippets.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
## Overview
66

7+
/// warning | Not Meant for User Facing Sites
8+
Snippets is meant to make including snippets in documentation easier, but it should not be used for user facing sites
9+
that take and parse user content dynamically.
10+
///
11+
712
Snippets is an extension to insert markdown or HTML snippets into another markdown file. Snippets is great for
813
situations where you have content you need to insert into multiple documents. For instance, this document keeps all its
914
hyperlinks in a separate file and then includes those hyperlinks at the bottom of a document via Snippets. If a link
@@ -260,3 +265,4 @@ Option | Type | Default | Description
260265
`url_timeout` | float | `#!py3 10.0` | Passes an arbitrary timeout in seconds to URL requestor. By default this is set to 10 seconds.
261266
`url_request_headers` | {string:string} | `#!py3 {}` | Passes arbitrary headers to URL requestor. By default this is set to empty map.
262267
`dedent_subsections` | bool | `#!py3 False` | Remove any common leading whitespace from every line in text of a subsection that is inserted via "sections" or by "lines".
268+
`restrict_base_path` | bool | `#!py True` | Ensure that the specified snippets are children of the specified base path(s). This prevents a path relative to the base path, but not explicitly a child of the base path.

pymdownx/__meta__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,5 @@ def parse_version(ver, pre=False):
185185
return Version(major, minor, micro, release, pre, post, dev)
186186

187187

188-
__version_info__ = Version(9, 11, 0, "final")
188+
__version_info__ = Version(10, 0, 0, "final")
189189
__version__ = __version_info__._get_canonical()

pymdownx/snippets.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ def __init__(self, config, md):
8282
base = config.get('base_path')
8383
if isinstance(base, str):
8484
base = [base]
85-
self.base_path = base
85+
self.base_path = [os.path.abspath(b) for b in base]
86+
self.restrict_base_path = config['restrict_base_path']
8687
self.encoding = config.get('encoding')
8788
self.check_paths = config.get('check_paths')
8889
self.auto_append = config.get('auto_append')
@@ -159,18 +160,22 @@ def get_snippet_path(self, path):
159160
for base in self.base_path:
160161
if os.path.exists(base):
161162
if os.path.isdir(base):
162-
filename = os.path.join(base, path)
163+
if self.restrict_base_path:
164+
filename = os.path.abspath(os.path.join(base, path))
165+
# If the absolute path is no longer under the specified base path, reject the file
166+
if not os.path.samefile(base, os.path.dirname(filename)):
167+
continue
168+
else:
169+
filename = os.path.join(base, path)
163170
if os.path.exists(filename):
164171
snippet = filename
165172
break
166173
else:
167-
basename = os.path.basename(base)
168174
dirname = os.path.dirname(base)
169-
if basename.lower() == path.lower():
170-
filename = os.path.join(dirname, path)
171-
if os.path.exists(filename):
172-
snippet = filename
173-
break
175+
filename = os.path.join(dirname, path)
176+
if os.path.exists(filename) and os.path.samefile(filename, base):
177+
snippet = filename
178+
break
174179
return snippet
175180

176181
@functools.lru_cache()
@@ -367,6 +372,10 @@ def __init__(self, *args, **kwargs):
367372

368373
self.config = {
369374
'base_path': [["."], "Base path for snippet paths - Default: [\".\"]"],
375+
'restrict_base_path': [
376+
True,
377+
"Restrict snippet paths such that they are under the base paths - Default: True"
378+
],
370379
'encoding': ["utf-8", "Encoding of snippets - Default: \"utf-8\""],
371380
'check_paths': [False, "Make the build fail if a snippet can't be found - Default: \"False\""],
372381
"auto_append": [
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Snippet

tests/test_extensions/test_snippets.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,63 @@ def test_user(self):
481481
)
482482

483483

484+
class TestSnippetsNested(util.MdCase):
485+
"""Test nested restriction."""
486+
487+
extension = [
488+
'pymdownx.snippets',
489+
]
490+
491+
extension_configs = {
492+
'pymdownx.snippets': {
493+
'base_path': os.path.join(BASE, '_snippets', 'nested'),
494+
'check_paths': True
495+
}
496+
}
497+
498+
def test_restricted(self):
499+
"""Test file restriction."""
500+
501+
with self.assertRaises(SnippetMissingError):
502+
self.check_markdown(
503+
R'''
504+
--8<-- "../b.txt"
505+
''',
506+
'''
507+
<p>Snippet</p>
508+
''',
509+
True
510+
)
511+
512+
513+
class TestSnippetsNestedUnrestricted(util.MdCase):
514+
"""Test nested no bounds."""
515+
516+
extension = [
517+
'pymdownx.snippets',
518+
]
519+
520+
extension_configs = {
521+
'pymdownx.snippets': {
522+
'base_path': os.path.join(BASE, '_snippets', 'nested'),
523+
'restrict_base_path': False
524+
}
525+
}
526+
527+
def test_restricted(self):
528+
"""Test file restriction."""
529+
530+
self.check_markdown(
531+
R'''
532+
--8<-- "../b.txt"
533+
''',
534+
'''
535+
<p>Snippet</p>
536+
''',
537+
True
538+
)
539+
540+
484541
class TestSnippetsAutoAppend(util.MdCase):
485542
"""Test snippet file case."""
486543

0 commit comments

Comments
 (0)