v2.17.1 — security hardening (draconian must-fix review)#321
Merged
Conversation
create_client_certificate() built the on-disk identifier directly from the caller-supplied Common Name (only spaces replaced) and ran cert_subdir.mkdir(parents=True) before anything else, so a CN such as '../../../../tmp/evil' created — and wrote a CA-signed key + cert + metadata into — a directory outside the managed cert tree. The existing _validate_identifier guard was only ever applied on retrieval, never to the value that constructs the identifier. Slugify the CN at the construction site (covers the single, batch, and any future caller) and assert containment before mkdir. Benign CNs slugify to themselves, so existing identifiers/paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
restore_unified_backup() set 0600 only for the exact name 'privkey.pem' and 0644 for everything else. certbot keeps the real key bytes in archive/<domain>/privkeyN.pem (live/privkey.pem symlinks to them) and the ACME account key in accounts/.../private_key.json — both retained in the unified backup — so restore actively downgraded served key material to world-readable. Match certbot's own permissions: lock every private-key filename to 0600 and leave public cert material (cert/chain/fullchain + metadata json) at 0644. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
notifications is a deep-merge key, but its secrets live in a list-of-dicts (channels.webhooks). _deep_merge_dict replaces lists wholesale and _strip_masked_values only walks dicts, so a GET (which masks secrets to the sentinel) followed by a POST of the whole settings blob through the generic /api/settings path persisted '********' over the real HMAC secret / bot token. Only the dedicated /api/notifications/config route restored list secrets. atomic_update now restores masked list secrets generically for every deep-merge key via _restore_masked_list_secrets_deep, so the generic path matches the dedicated route. Silent corruption (deliveries would sign with '********') is closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MetricsList carried no auth decorator while its sibling info endpoints
(CacheStats, BackupList) are require_role('viewer'), so the endpoint was
reachable unauthenticated. Gate it to match its peers; the Prometheus scrape
target is the separate public '/metrics' route.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…deleted create_session() snapshots the role at login and validate_session() returned that frozen snapshot with only an expiry check, so disabling, demoting, or deleting a user had no effect on an already-issued session — it stayed valid (with the old role) for up to SESSION_TIMEOUT_HOURS, with no kill switch for a compromised or just-removed account. update_user (on enabled=False or a role change) and delete_user now invalidate the user's in-memory sessions, forcing re-authentication under the new state. An unrelated edit (e.g. email) does not log the user out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundles five must-fix findings from a draconian security/logic review: client-cert Common Name path traversal, backup-restore private-key 0644 downgrade, unauthenticated GET /api/metrics, stale session on user disable/demote/delete, and webhook-secret clobbering on the generic settings save path. No new features; each fix has a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #321 +/- ##
==========================================
+ Coverage 70.67% 70.85% +0.17%
==========================================
Files 50 50
Lines 11548 11589 +41
==========================================
+ Hits 8162 8211 +49
+ Misses 3386 3378 -8
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v2.17.1 — security hardening (draconian must-fix review)
Five must-fix findings from a draconian security/logic review of the existing code. No new features. Each was verified against source (refute-first: only findings that survived independent skeptics) and is pinned by a regression test.
Security
Path traversal via client-certificate Common Name (
modules/core/client_certificates.py).create_client_certificatebuilt the on-disk identifier straight from the caller-supplied Common Name (only spaces replaced) and ranmkdir(parents=True)before any other check, so a CN such as../../../../tmp/evilcreated — and wrote a CA-signed key, certificate and metadata into — a directory outside the managed cert tree (single and batch issuance, operator role). The pre-existing_validate_identifierguard was only ever applied on retrieval, never to the value that constructs the identifier. Fixed by slugifying the CN at the construction site (covering every caller) plus a containment assertion beforemkdir. Benign CNs slugify to themselves, so existing identifiers/paths are unchanged and the full CN is still preserved verbatim in the subject and metadata.Backup restore downgraded private keys to world-readable (
modules/core/file_operations.py).restore_unified_backupset0600only for the exact nameprivkey.pemand0644for everything else, but certbot keeps the real key bytes inarchive/<domain>/privkeyN.pem(thelive/privkey.pemsymlink points at them) and the ACME account key inaccounts/.../private_key.json— both retained in the unified backup — so a restore actively rewrote served key material and the account key to0644. Now mirrors certbot's own permissions: every private-key filename →0600, public material (cert/chain/fullchain + metadata json) stays0644.GET /api/metricsreachable unauthenticated (modules/api/resources.py). The endpoint carried no auth decorator while its sibling info endpoints (/api/cache,/api/backups) require theviewerrole. Gated to match its peers; the Prometheus scrape target remains the separate, intentionally public/metricsroute.Disabled / demoted / deleted user kept their live session (
modules/core/auth.py). The role was snapshotted into the session at login and validation only checked expiry, so disabling, demoting, or deleting a user left an already-issued session valid — with the old role — for up to the session lifetime (default 8h), with no kill switch. Those transitions now invalidate the user's in-memory sessions; an unrelated edit (e.g. email) does not log the user out.Fixes
modules/core/settings.py). v2.15.0 restored masked webhook secrets on the dedicated/api/notifications/configroute, but the genericPOST /api/settingspath still merged thenotificationssubtree without restoring secrets nested in thechannels.webhookslist (_deep_merge_dictreplaces lists wholesale;_strip_masked_valuesonly walks dicts), writing the mask sentinel over the real HMAC secret / bot token. Silent corruption — deliveries then signed with********.atomic_updatenow restores masked list secrets generically for every deep-merge key, so both paths behave identically.Validation
certmate.orgsubdomains, issuer pinned to staging on the issued leaf: 13 passed (async issuance, full lifecycle, force-renew, reissue incl. path-traversal rejection).🤖 Generated with Claude Code