mkdocs-easylinks-plugin 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/CHANGELOG.md +9 -0
  2. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/PKG-INFO +1 -1
  3. mkdocs_easylinks_plugin-0.2.3/mkdocs_easylinks/__init__.py +8 -0
  4. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks/plugin.py +30 -5
  5. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks_plugin.egg-info/PKG-INFO +1 -1
  6. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/pyproject.toml +1 -1
  7. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/tests/test_plugin.py +38 -0
  8. mkdocs_easylinks_plugin-0.2.2/mkdocs_easylinks/__init__.py +0 -3
  9. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/.github/workflows/publish.yml +0 -0
  10. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/.github/workflows/test.yml +0 -0
  11. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/.gitignore +0 -0
  12. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/LICENSE +0 -0
  13. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/README.md +0 -0
  14. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks_plugin.egg-info/SOURCES.txt +0 -0
  15. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks_plugin.egg-info/dependency_links.txt +0 -0
  16. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks_plugin.egg-info/entry_points.txt +0 -0
  17. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks_plugin.egg-info/requires.txt +0 -0
  18. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/mkdocs_easylinks_plugin.egg-info/top_level.txt +0 -0
  19. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/setup.cfg +0 -0
  20. {mkdocs_easylinks_plugin-0.2.2 → mkdocs_easylinks_plugin-0.2.3}/tests/__init__.py +0 -0
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.3] - 2026-04-29
6
+
7
+ ### Security
8
+ - Expanded log sanitizer to escape ANSI escape sequences, NUL bytes, vertical tab, form feed, and Unicode line separators (U+2028/U+2029) in addition to `\n`, `\r`, and `\t`
9
+ - Path safety check now rejects absolute and drive-relative paths (e.g. `/etc/passwd`, `C:\…`, `\\server\share\…`) in addition to `..` traversal
10
+
11
+ ### Fixed
12
+ - `mkdocs_easylinks.__version__` is now read from installed package metadata, eliminating drift between `pyproject.toml` and the module attribute
13
+
5
14
  ## [0.2.2] - 2026-04-13
6
15
 
7
16
  ### Security
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-easylinks-plugin
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: An MkDocs plugin that allows linking to files by filename only
5
5
  Author: Daniel Ferguson
6
6
  License-Expression: MIT
@@ -0,0 +1,8 @@
1
+ """MkDocs EasyLinks Plugin - Simplified cross-referencing by filename."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("mkdocs-easylinks-plugin")
7
+ except PackageNotFoundError: # pragma: no cover
8
+ __version__ = "unknown"
@@ -17,9 +17,28 @@ from mkdocs.config.defaults import MkDocsConfig
17
17
  logger = logging.getLogger("mkdocs.plugins.easylinks")
18
18
 
19
19
 
20
+ _LOG_ESCAPES = {"\n": "\\n", "\r": "\\r", "\t": "\\t"}
21
+
22
+
20
23
  def _sanitize_log(value: str) -> str:
21
- """Strip control characters from user-controlled strings before logging."""
22
- return value.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
24
+ """Escape control characters and other non-printable codepoints before logging.
25
+
26
+ Filenames can legally contain ANSI escape sequences, NULs, vertical tabs,
27
+ form feeds, or Unicode line separators on POSIX systems. Logging them
28
+ verbatim would let an attacker spoof log lines, recolor output, or
29
+ manipulate a terminal that tails the build log.
30
+ """
31
+ out = []
32
+ for c in value:
33
+ if c in _LOG_ESCAPES:
34
+ out.append(_LOG_ESCAPES[c])
35
+ elif c.isprintable():
36
+ out.append(c)
37
+ elif ord(c) <= 0xff:
38
+ out.append(f"\\x{ord(c):02x}")
39
+ else:
40
+ out.append(f"\\u{ord(c):04x}")
41
+ return "".join(out)
23
42
 
24
43
 
25
44
  class EasyLinksConfig(Config):
@@ -128,10 +147,16 @@ class EasyLinksPlugin(BasePlugin[EasyLinksConfig]):
128
147
  """Return True if src_path stays within the docs root.
129
148
 
130
149
  MkDocs supplies src_path as a relative path (e.g. ``subdir/page.md``).
131
- A path that escapes the docs root after normalization (e.g.
132
- ``../../etc/passwd``) would produce a traversal link in the output and
133
- must be rejected.
150
+ Absolute paths (``/etc/passwd``, ``C:\\Windows\\...``, UNC paths) and
151
+ relative paths that escape the docs root after normalization (e.g.
152
+ ``../../etc/passwd``) would both produce traversal links in the
153
+ output and must be rejected.
134
154
  """
155
+ # os.path.isabs alone is not enough: on Python 3.13+ Windows it returns
156
+ # False for drive-relative paths like ``/etc/passwd`` or ``\foo``, which
157
+ # would still escape the docs root once a drive is resolved.
158
+ if os.path.isabs(src_path) or src_path.startswith(("/", "\\")):
159
+ return False
135
160
  if ".." not in src_path:
136
161
  return True
137
162
  return not os.path.normpath(src_path).startswith("..")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-easylinks-plugin
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: An MkDocs plugin that allows linking to files by filename only
5
5
  Author: Daniel Ferguson
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mkdocs-easylinks-plugin"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "An MkDocs plugin that allows linking to files by filename only"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1182,6 +1182,31 @@ Regular paragraph with [example link](example.md).
1182
1182
  from mkdocs_easylinks.plugin import _sanitize_log
1183
1183
  assert _sanitize_log("a\nb\rc\td") == "a\\nb\\rc\\td"
1184
1184
 
1185
+ def test_sanitize_log_escapes_ansi_escape(self):
1186
+ """_sanitize_log must escape ESC so terminals tailing logs cannot be hijacked."""
1187
+ from mkdocs_easylinks.plugin import _sanitize_log
1188
+ assert _sanitize_log("file\x1b[31mRED.md") == "file\\x1b[31mRED.md"
1189
+
1190
+ def test_sanitize_log_escapes_null_byte(self):
1191
+ """_sanitize_log must escape NUL bytes."""
1192
+ from mkdocs_easylinks.plugin import _sanitize_log
1193
+ assert _sanitize_log("file\x00.md") == "file\\x00.md"
1194
+
1195
+ def test_sanitize_log_escapes_vertical_tab_and_form_feed(self):
1196
+ """_sanitize_log must escape \\v and \\f, which can move the cursor in some terminals."""
1197
+ from mkdocs_easylinks.plugin import _sanitize_log
1198
+ assert _sanitize_log("a\vb\fc") == "a\\x0bb\\x0cc"
1199
+
1200
+ def test_sanitize_log_escapes_unicode_line_separator(self):
1201
+ """_sanitize_log must escape U+2028 and U+2029, which act as line breaks in some viewers."""
1202
+ from mkdocs_easylinks.plugin import _sanitize_log
1203
+ assert _sanitize_log("a
b
c") == "a\\u2028b\\u2029c"
1204
+
1205
+ def test_sanitize_log_preserves_printable_unicode(self):
1206
+ """_sanitize_log must leave printable Unicode (accents, emoji, CJK) alone."""
1207
+ from mkdocs_easylinks.plugin import _sanitize_log
1208
+ assert _sanitize_log("café-日本語-🚀.md") == "café-日本語-🚀.md"
1209
+
1185
1210
  # ------------------------------------------------------------------
1186
1211
  # Security fix: path traversal (_is_safe_path / on_files)
1187
1212
  # ------------------------------------------------------------------
@@ -1210,6 +1235,19 @@ Regular paragraph with [example link](example.md).
1210
1235
  """An embedded .. that stays within the root should be accepted."""
1211
1236
  assert self.plugin._is_safe_path("subdir/../other/file.md") is True
1212
1237
 
1238
+ def test_is_safe_path_rejects_posix_absolute(self):
1239
+ """An absolute POSIX path must be rejected even though it has no '..'."""
1240
+ assert self.plugin._is_safe_path("/etc/passwd") is False
1241
+
1242
+ def test_is_safe_path_rejects_windows_absolute(self):
1243
+ """An absolute Windows path must be rejected when running on Windows."""
1244
+ import os
1245
+ import pytest
1246
+ if os.name != "nt":
1247
+ pytest.skip("Windows-specific path semantics")
1248
+ assert self.plugin._is_safe_path(r"C:\Windows\System32\config\SAM") is False
1249
+ assert self.plugin._is_safe_path(r"\\server\share\file.md") is False
1250
+
1213
1251
  def test_traversal_path_ignored_in_on_files(self):
1214
1252
  """Files whose src_path escapes the docs root must be excluded from the index."""
1215
1253
  mock_config = MagicMock()
@@ -1,3 +0,0 @@
1
- """MkDocs EasyLinks Plugin - Simplified cross-referencing by filename."""
2
-
3
- __version__ = "0.1.4"