Skip to content

fix: Prevent symlink following privilege escalation in nbde_server_tang module#240

Merged
richm merged 6 commits into
linux-system-roles:mainfrom
spetrosi:fix-symlink-cve
Jun 23, 2026
Merged

fix: Prevent symlink following privilege escalation in nbde_server_tang module#240
richm merged 6 commits into
linux-system-roles:mainfrom
spetrosi:fix-symlink-cve

Conversation

@spetrosi

@spetrosi spetrosi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Enhancement: Added symlink protection to prevent privilege escalation vulnerability in the set_file_ownership_and_perms() function.

Reason: The module was using os.chown() which follows symlinks. An unprivileged local user could pre-stage symlinks in tang keydir pointing to sensitive files. When the module runs as root, it would follow the symlinks and change ownership of arbitrary files to the tang user, enabling privilege escalation.

Result: The module now:

  1. Explicitly checks and skips symlinks before processing files
  2. Uses os.lchown() instead of os.chown() for defense in depth (lchown does not follow symlinks)

This prevents the symlink-following attack vector while maintaining the intended functionality of setting ownership and permissions on legitimate tang key files.

Security Impact: Prevents local privilege escalation attack where unprivileged users could manipulate file ownership through symlink attacks.

Summary by CodeRabbit

  • Bug Fixes

    • Hardened ownership and permission updates to safely skip symbolic links (including .jwk symlink handling) and avoid following them, preventing unintended changes to link targets.
  • Tests

    • Added unit and integration coverage for symlink protection, error handling, check-mode behavior, and non-.jwk/non-directory edge cases.
  • Chores

    • Enabled an additional pytest requirement for older Python versions and added a minimal unit test package initializer.

…ng module

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ed4bcea-e2a1-4f38-a355-098478e38ed4

📥 Commits

Reviewing files that changed from the base of the PR and between b78f704 and cacc266.

📒 Files selected for processing (1)
  • tests/unit/test_nbde_server_tang.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/test_nbde_server_tang.py

📝 Walkthrough

Walkthrough

set_file_ownership_and_perms now skips symlink entries and opens files with os.O_NOFOLLOW, applying ownership and mode changes via os.fchown/os.fchmod on the file descriptor and always closing the descriptor. Unit and integration tests and a pytest mock requirement were added.

Changes

Symlink protection and tests

Layer / File(s) Summary
Implementation: open with O_NOFOLLOW and use fd-based ownership
library/nbde_server_tang.py
Skip symlink entries, open files with os.O_NOFOLLOW, apply ownership/permissions with os.fchown/os.fchmod on the fd, handle open failures by skipping, and always close the fd.
Unit and integration tests for symlink protection
tests/unit/test_nbde_server_tang.py, tests/unit/__init__.py
Add test module init with SPDX header and imports; implement unit tests mocking symlink detection, O_NOFOLLOW failures, fchown/fchmod error paths, descriptor-closing guarantees, non-.jwk file filtering, and check_mode/non-directory early exits; add integration test using a temp directory to confirm real symlink skipping and regular file mode updates; include script entrypoint.
Test dependency activation
pytest_extra_requirements.txt
Uncomment/activate the mock ; python_version < "3.0" pytest extra requirement entry.
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title follows Conventional Commits format with 'fix:' type and clearly describes the main security fix for symlink protection in the nbde_server_tang module.
Description check ✅ Passed The pull request description follows the required template with Enhancement, Reason, Result, and Security Impact sections, providing comprehensive details about the vulnerability, solution, and impact.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Format ✅ Passed PR description contains all required template sections: Enhancement, Reason, and Result as specified in .github/pull_request_template.md.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
library/nbde_server_tang.py (1)

313-318: ⚡ Quick win

Add test coverage for symlink attack prevention.

The symlink protection logic should be validated with dedicated tests to ensure the security fix is effective and prevent future regressions.

Consider adding tests that:

  1. Pre-stage a symlink pointing to a sensitive test file in the keydir
  2. Invoke the module's key deployment/rotation operations
  3. Assert that the sensitive file's ownership and permissions remain unchanged
  4. Verify that legitimate .jwk files (non-symlinks) are processed correctly

This would provide confidence that the symlink attack vector is properly mitigated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/nbde_server_tang.py` around lines 313 - 318, Add unit tests that
validate the symlink protection around the file-processing loop: create a
temporary keydir, place a sensitive target file outside the keydir and a symlink
inside the keydir that points to that sensitive file, then invoke the module
function that iterates files (the code path using os.path.islink(fname),
os.lchown(fname, uid, gid) and os.chmod(fname, 0o400)) and assert the sensitive
target's ownership and permissions are unchanged; also add a control test where
a real non-symlink `.jwk` file in the keydir is processed and its
ownership/permissions are updated as expected to ensure legitimate files are
handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/nbde_server_tang.py`:
- Around line 313-318: There's a TOCTOU race between os.path.islink(fname) and
os.chmod(fname) because chmod follows symlinks; fix by opening the file
atomically with O_NOFOLLOW and operating on the file descriptor: use
os.open(fname, flags | os.O_NOFOLLOW) (handle ELOOP/EINVAL by skipping), then
call os.fchown(fd, uid, gid) and os.fchmod(fd, 0o400) on that fd, finally close
the fd; keep the existing os.path.islink/os.lchown checks only as
defense-in-depth but perform the ownership and mode changes via os.open +
os.fchown + os.fchmod to eliminate the race.
- Around line 313-318: File nbde_server_tang.py fails flake8/black: fix long
lines (E501) and non-top imports (E402) and ensure formatting matches black.
Shorten lines around the reported locations by breaking long string literals,
chained calls, or long argument lists into multiple lines (respecting black
line-length defaults), move any imports currently inside functions or blocks up
to the module top (resolve E402) and remove/adjust any unused imports; for the
os.lchown/os.chmod block ensure lines are within length limits and keep the
symlink check logic unchanged, then run black and flake8 locally (or tox -e
black,flake8) to confirm all violations are resolved before committing.

---

Nitpick comments:
In `@library/nbde_server_tang.py`:
- Around line 313-318: Add unit tests that validate the symlink protection
around the file-processing loop: create a temporary keydir, place a sensitive
target file outside the keydir and a symlink inside the keydir that points to
that sensitive file, then invoke the module function that iterates files (the
code path using os.path.islink(fname), os.lchown(fname, uid, gid) and
os.chmod(fname, 0o400)) and assert the sensitive target's ownership and
permissions are unchanged; also add a control test where a real non-symlink
`.jwk` file in the keydir is processed and its ownership/permissions are updated
as expected to ensure legitimate files are handled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d5b880d8-49c4-4b09-8e82-10d40b90b9ce

📥 Commits

Reviewing files that changed from the base of the PR and between 690459f and f582e11.

📒 Files selected for processing (1)
  • library/nbde_server_tang.py

Comment thread library/nbde_server_tang.py Outdated
spetrosi and others added 2 commits June 12, 2026 18:01
Use O_NOFOLLOW with file descriptor operations (fchown/fchmod) to
atomically prevent symlink following, eliminating the race condition
between islink() check and chmod() operation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add comprehensive unit tests covering:
- Normal file ownership changes with O_NOFOLLOW
- Symlink detection and skipping via islink() check
- TOCTOU race protection via O_NOFOLLOW
- Error handling (OSError, IOError)
- File descriptor cleanup on error
- Integration test with real symlinks

Tests verify both Python 2.7 and 3.x compatibility.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Comment thread tests/unit/test_nbde_server_tang.py Fixed
Comment thread tests/unit/test_nbde_server_tang.py Fixed
Comment thread tests/unit/test_nbde_server_tang.py Fixed
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@690459f). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #240   +/-   ##
=======================================
  Coverage        ?   22.95%           
=======================================
  Files           ?        2           
  Lines           ?      196           
  Branches        ?        0           
=======================================
  Hits            ?       45           
  Misses          ?      151           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

spetrosi and others added 2 commits June 12, 2026 18:17
- Remove shebang from unit test file (ansible-test requirement)
- Remove unused 'call' import (pylint unused-import)
- Replace insecure tempfile.mktemp with NamedTemporaryFile (ansible-bad-function)
- Apply black formatting

Fixes ansible-test pylint and shebang sanity checks.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Enable mock package installation for Python 2.7 test environment.
Python 2.7 does not have unittest.mock in stdlib, requires external
mock package.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@spetrosi spetrosi marked this pull request as draft June 12, 2026 16:27
@spetrosi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/unit/test_nbde_server_tang.py (1)

304-308: 💤 Low value

Move import shutil to module top.

Importing inside a method works but deviates from PEP 8 conventions which recommend placing all imports at the top of the file. This also avoids potential import overhead on each tearDown call.

♻️ Suggested change

Add at the top of the file with other imports:

import shutil

Then simplify tearDown:

     def tearDown(self):
         """Clean up test directory."""
-        import shutil
-
         shutil.rmtree(self.test_dir, ignore_errors=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_nbde_server_tang.py` around lines 304 - 308, The tearDown
method currently imports shutil locally; move "import shutil" to the
top-of-module imports and remove the in-method import so tearDown simply calls
shutil.rmtree(self.test_dir, ignore_errors=True). Update references in the
tearDown function (method name: tearDown) to use the module-level shutil import.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/test_nbde_server_tang.py`:
- Around line 304-308: The tearDown method currently imports shutil locally;
move "import shutil" to the top-of-module imports and remove the in-method
import so tearDown simply calls shutil.rmtree(self.test_dir,
ignore_errors=True). Update references in the tearDown function (method name:
tearDown) to use the module-level shutil import.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c0c40b1-5668-4ea3-8eee-405d16346a64

📥 Commits

Reviewing files that changed from the base of the PR and between f582e11 and b78f704.

📒 Files selected for processing (4)
  • library/nbde_server_tang.py
  • pytest_extra_requirements.txt
  • tests/unit/__init__.py
  • tests/unit/test_nbde_server_tang.py
✅ Files skipped from review due to trivial changes (2)
  • pytest_extra_requirements.txt
  • tests/unit/init.py

Move shutil import from tearDown method to top-level imports
for consistency with Python best practices.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@spetrosi

Copy link
Copy Markdown
Contributor Author

[citest]

@spetrosi spetrosi marked this pull request as ready for review June 15, 2026 14:39
@richm richm merged commit 065b441 into linux-system-roles:main Jun 23, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants