Skip to content
Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
facelessuser committed May 14, 2023
1 parent a8fb966 commit b7bb487
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 9 deletions.
6 changes: 6 additions & 0 deletions docs/src/markdown/about/changelog.md
@@ -1,5 +1,11 @@
# Changelog

## 10.0

- **Break**: Snippets: snippets will restrict snippets to ensure they are under the `base_path` preventing snippets
relative to the `base_path` but not explicitly under it. `restrict_base_path` can be set to `False` for legacy
behavior.

## 9.11

- **NEW**: Emoji: Update to new CDN and use Twemoji 14.1.2.
Expand Down
6 changes: 6 additions & 0 deletions docs/src/markdown/extensions/snippets.md
Expand Up @@ -4,6 +4,11 @@

## Overview

/// warning | Not Meant for User Facing Sites
Snippets is meant to make including snippets in documentation easier, but it should not be used for user facing sites
that take and parse user content dynamically.
///

Snippets is an extension to insert markdown or HTML snippets into another markdown file. Snippets is great for
situations where you have content you need to insert into multiple documents. For instance, this document keeps all its
hyperlinks in a separate file and then includes those hyperlinks at the bottom of a document via Snippets. If a link
Expand Down Expand Up @@ -260,3 +265,4 @@ Option | Type | Default | Description
`url_timeout` | float | `#!py3 10.0` | Passes an arbitrary timeout in seconds to URL requestor. By default this is set to 10 seconds.
`url_request_headers` | {string:string} | `#!py3 {}` | Passes arbitrary headers to URL requestor. By default this is set to empty map.
`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".
`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.
2 changes: 1 addition & 1 deletion pymdownx/__meta__.py
Expand Up @@ -185,5 +185,5 @@ def parse_version(ver, pre=False):
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(9, 11, 0, "final")
__version_info__ = Version(10, 0, 0, "final")
__version__ = __version_info__._get_canonical()
25 changes: 17 additions & 8 deletions pymdownx/snippets.py
Expand Up @@ -82,7 +82,8 @@ def __init__(self, config, md):
base = config.get('base_path')
if isinstance(base, str):
base = [base]
self.base_path = base
self.base_path = [os.path.abspath(b) for b in base]
self.restrict_base_path = config['restrict_base_path']
self.encoding = config.get('encoding')
self.check_paths = config.get('check_paths')
self.auto_append = config.get('auto_append')
Expand Down Expand Up @@ -159,18 +160,22 @@ def get_snippet_path(self, path):
for base in self.base_path:
if os.path.exists(base):
if os.path.isdir(base):
filename = os.path.join(base, path)
if self.restrict_base_path:
filename = os.path.abspath(os.path.join(base, path))
# If the absolute path is no longer under the specified base path, reject the file
if not os.path.samefile(base, os.path.dirname(filename)):
continue
else:
filename = os.path.join(base, path)
if os.path.exists(filename):
snippet = filename
break
else:
basename = os.path.basename(base)
dirname = os.path.dirname(base)
if basename.lower() == path.lower():
filename = os.path.join(dirname, path)
if os.path.exists(filename):
snippet = filename
break
filename = os.path.join(dirname, path)
if os.path.exists(filename) and os.path.samefile(filename, base):
snippet = filename
break
return snippet

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

self.config = {
'base_path': [["."], "Base path for snippet paths - Default: [\".\"]"],
'restrict_base_path': [
True,
"Restrict snippet paths such that they are under the base paths - Default: True"
],
'encoding': ["utf-8", "Encoding of snippets - Default: \"utf-8\""],
'check_paths': [False, "Make the build fail if a snippet can't be found - Default: \"False\""],
"auto_append": [
Expand Down
1 change: 1 addition & 0 deletions tests/test_extensions/_snippets/nested/nested.txt
@@ -0,0 +1 @@
Snippet
57 changes: 57 additions & 0 deletions tests/test_extensions/test_snippets.py
Expand Up @@ -481,6 +481,63 @@ def test_user(self):
)


class TestSnippetsNested(util.MdCase):
"""Test nested restriction."""

extension = [
'pymdownx.snippets',
]

extension_configs = {
'pymdownx.snippets': {
'base_path': os.path.join(BASE, '_snippets', 'nested'),
'check_paths': True
}
}

def test_restricted(self):
"""Test file restriction."""

with self.assertRaises(SnippetMissingError):
self.check_markdown(
R'''
--8<-- "../b.txt"
''',
'''
<p>Snippet</p>
''',
True
)


class TestSnippetsNestedUnrestricted(util.MdCase):
"""Test nested no bounds."""

extension = [
'pymdownx.snippets',
]

extension_configs = {
'pymdownx.snippets': {
'base_path': os.path.join(BASE, '_snippets', 'nested'),
'restrict_base_path': False
}
}

def test_restricted(self):
"""Test file restriction."""

self.check_markdown(
R'''
--8<-- "../b.txt"
''',
'''
<p>Snippet</p>
''',
True
)


class TestSnippetsAutoAppend(util.MdCase):
"""Test snippet file case."""

Expand Down

0 comments on commit b7bb487

Please sign in to comment.