[2.8] Add class allow-list migration controls - #4888
Conversation
Greptile SummaryThis PR adds migration controls for component class authorization in 2.8. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (9): Last reviewed commit: "Clarify class allow-list migration paths" | Re-trigger Greptile |
The _get_allow_list / _get_allow_list_from_file / _get_allow_list_from_resources wrappers only delegated to the _get_policy_* variants and had a single test caller left. Fold that test onto _get_policy_from_file and remove the shims so the planned cherry-pick to main carries the clean version. Also document the cache tuple shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Issues and suggestions
-
Audit event is silently dropped when no auditor is initialized, but dedup still marks it as recorded (component_path_authorizer.py, _audit_allow_all).
AuditService.add_event returns "" and does nothing if the_auditor is None — notably, the simulator never initializes it (simulator_worker.py:256 has the call commented out). The code adds audit_key to _allow_all_audit_keys regardless of whether the event was actually written, so the audit is never retried. The logger.warning still fires once, so there's a log trace, but the PR's stated guarantee ("records an audit event") doesn't hold in simulator runs and in any window before auditor init. Consider checking the return value and only adding to the dedup set on success (falling back to warning-only dedup otherwise), or at least noting the simulator behavior in the docs. -
Warn mode has no audit trail — inconsistent with the wildcard path.
Both features "relax the 2.8 protection" (the docs say so explicitly), but only the wildcard records an audit event. In warn mode, each unmatched class that gets loaded is only a site-log warning. From an auditor's perspective these are exactly the events worth recording — a specific unauthorized class was allowed to load. Suggest adding an AuditService.add_event (deduplicated per job + class path) on the warn-mode allow path, mirroring _ALLOW_ALL_AUDIT_ACTION. -
Audit file I/O performed while holding _allow_all_audit_lock.
Auditor.add_event does a write + flush inside the lock. Contention is low (dedup means it's rare), so this is minor — but moving the add_event call outside the lock (check-and-mark inside, emit outside) is a small cleanup. Note that would allow a duplicate under a race, which may be an acceptable trade; current code is correct, just holds a lock across file I/O. -
Redundant file stat on every wildcard-mode component build.
Under the wildcard, every authorize_component_config call runs _get_policy (stat + cache check) and then _audit_allow_all, which calls _get_resources_file_path again (another os.path.exists/stat) and takes the audit lock even after dedup. Cheap per-call, but it runs once per component per job config parse. Passing the already-resolved resources_file from _get_policy into _audit_allow_all would remove the duplicate resolution. -
Warn-mode logging bypasses the FL logging convention.
self.logger.warning(...) works, but FLComponent.log_warning(fl_ctx, msg) would include job/identity context in the log line, which matters here since the whole point of warn mode is inventorying classes per job. fl_ctx is Optional in this path, so it needs a guard, but the event-driven path always has one. -
Minor test gaps.
- No test that a missing class_allow_list still errors when class_list_enforcement_mode: "warn" is set — the release notes explicitly promise this ("remains a configuration error … in either enforcement mode").
- No test for wildcard loaded via the workspace resources.json file path (the wildcard tests all go through ConfigService), so the 3-tuple cache round-trip for the wildcard/mode combination isn't exercised end-to-end.
- Docs nit.
The wildcard example in flare_280.rst shows "class_list_enforcement_mode": "enforce" next to "class_allow_list": [""]. It's technically accurate (mode is irrelevant once the wildcard is present) but slightly confusing — a reader may think enforce is required for the wildcard to audit. A one-line note that the mode has no effect when "" is present would help.
Security Review: PR #4888 — class allow-list migration controls
Threat model context
The control being relaxed exists to protect a site from malicious or compromised jobs: component configs pushed from the server instantiate classes by dotted path, so an unrestricted path is arbitrary code execution on the site (any importable class with dangerous constructor side effects — the tests' own subprocess.Popen example). The right question is: can anyone other than the site admin turn these relaxations on, and what's the blast radius when the admin does?
Findings (by severity)
S1 — Warn mode is site-global and unbounded, with no audit trail (medium).
Enabling warn to migrate one application opens the site to arbitrary class loading by every non-BYOC job from the server, indefinitely — and in FL the server/job-submitter isprecisely the adversary this control defends against. The only record of an unauthorized class actually being loaded is a site log warning; site logs rotate and are not the tamper-evident audit channel. Contrast: the wildcard — the other relaxation — does get an audit event. The event that most warrants auditing ("class X outside the allow-list wasallowed to load for job Y") is the one that isn't recorded. Recommend: emit an AuditService.add_event on every warn-mode allow (dedup per job+class, mirroring the wildcard pattern). Per-job or time-boxed warn scoping would be a worthwhile follow-up, but auditing is the minimum for merge.
S2 — Wildcard audit event can be silently lost while dedup marks it recorded (medium).
AuditService.add_event returns "" and does nothing when no auditor is initialized (never initialized in simulator; simulator_worker.py:256 is commented out) or after the audit file closes. _audit_allow_all adds the dedup key unconditionally, so a dropped event is never retried. The PR's own security guarantee ("records an audit event statingenforcement was bypassed") is best-effort in practice. Recommend: only add to the dedup set when add_event returns a non-empty event id; keep the log warning deduplicated separately.
S3 — Job-controlled strings flow unsanitized into logs that drive security decisions (low-medium).
component_path is validated only as a non-empty str (get_component_path), so a job config can supply a path containing newlines or ANSI escapes. In warn mode that string is interpolated into the warning line admins are told to use to inventory and build their allow list — a malicious job can inject forged log lines (e.g., a fake "not in allow_list"warning for a dangerous class, nudging an admin to whitelist it). Enforce-mode exception messages have the same exposure pre-existing, but warn mode elevates these logs into a decision input, which is why it matters now. Recommend: validate component_path against a dotted-identifier pattern (^[A-Za-z]\w*(.[A-Za-z_]\w*)+$) in _get_component_path —every legitimately importable path matches, and it eliminates the injection class entirely. Same treatment for node_path or log it via repr().
S4 — Audit event omits the source of the wildcard (low).
_audit_allow_all computes source (the resolved resources file path) for the dedup key but doesn't put it in the event's msg or ref. For forensics ("which config file on whichsite had *, and since when"), the event should name the file. One-line fix: append source to msg.
A typo'd or forgotten class_allow_list key silently downgraded a site's configured policy to the built-in default; log a one-time warning so operators notice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pcnudde
left a comment
There was a problem hiding this comment.
I think the first option alone is enough.
|
@chesterxgchen The security-review findings are addressed in
The suggested missing-list error test was superseded by the accepted behavior that an omitted |
|
@chesterxgchen Item-by-item follow-up for the review, addressed in
Security findings:
The later simulator P1 is also addressed: audit scope now falls back from Validation: 150 relevant tests passed and |
|
Detailed update for the latest review feedback:
Validation completed with Python 3.12:
The minor informational notes about |
chesterxgchen
left a comment
There was a problem hiding this comment.
YT has a fair question.
### Description Cherry-pick of #4888 (merged to `2.8` as c3daf63) onto `main`, bringing the class allow-list migration controls forward: - support `"*"` in `class_allow_list` to allow all component classes, ignore remaining entries, and record an audit event - add `class_list_enforcement_mode` with `enforce` (default) and `warn` behavior - include the enforcement mode in provisioned client and server resource templates - use the curated built-in default allow list (with an audit event) when a site does not configure `class_allow_list` - unit coverage for wildcard, warn/enforce modes, site configuration filtering, and provisioning defaults **Conflict resolutions vs `main`** (which had diverged via #4841): - `master_template.yml`: kept main's `{~~class_allow_list~~}` placeholder (filled from `DEFAULT_CLASS_ALLOW_LIST` by the static file builder) and added only the new `class_list_enforcement_mode` line to both client and server resource blocks - `default_component_policy.py`: kept main's copy (identical list); updated its docstring since the authorizer now does use it as the implicit default - `static_file_builder_test.py`: kept main's `DEFAULT_CLASS_ALLOW_LIST`-based assertion instead of the PR's inline list; the new enforcement-mode assertion is included - `unsafe_component_detection.rst`: adopted the PR's default-with-audit semantics, keeping main's provisioning and `SimEnv` sentences Note: this intentionally changes main's #4841 behavior for unconfigured sites from "fail with an explicit setup error" to "use the curated built-in default and record an audit event", matching 2.8. Main's no-fallback tests were superseded by the PR's `test_uses_default_allow_list_*` tests. ### Types of changes - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [ ] Breaking change (fix or new feature that would cause existing functionality to change). - [x] New tests added to cover the changes. - [x] Quick tests passed locally by running `./runtest.sh`. - [x] In-line docstrings updated. - [x] Documentation updated. Validation: 155 passed, 1 skipped across all unit suites referencing the policy/authorizer; flake8 and black clean on changed files. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Peter Cnudde <pcnudde@nvidia.com>
What changed
"*"inclass_allow_listto allow all component classes, ignore remaining entries, and record an audit eventclass_list_enforcement_modewithenforce(default) andwarnbehaviorWhy
NVFLARE 2.8 adds component class authorization for non-BYOC jobs. Applications migrating from 2.7 need a secure default plus explicit transition options while they inventory and configure application classes. BYOC-enabled users and jobs continue to bypass the built-in class allow-list check, preserving their 2.7 behavior.
Validation
./runtest.sh -sgit diff --checkThe full strict Sphinx build parsed the new release-note content but continues to report 585 pre-existing documentation warnings elsewhere in the repository.