Class ClassloadFilterAudit
SecurityPolicy.isSafeClassName(String) used to
enforce before D-12/D-13, and records what each removed layer WOULD have refused -- it refuses
nothing itself.
Package-private on purpose. This class holds the four lists SecurityPolicy used
to own directly (moved here once SecurityPolicy.addTrustedPackage(String) and
SecurityPolicy.addDangerousClass(String) became no-ops, so these now reflect exactly
what the framework as shipped would have judged, never a caller's runtime mutation). Same
package is the low-surface choice: the alternative would have been making the lists
public purely so a class outside utils could read them.
This evaluator never decides. classify(String) returns which layer would have
refused a class name, or null for none -- never a boolean verdict, and nothing here can
be used by a caller to refuse a class. It exists to answer, after release, "did removing these
layers actually let anything through?" from real data rather than from an assertion.
Cannot reuse RemoteActionLog. UltiTools.java's bootstrap runs the
classloading scan (initPluginModules()) before it constructs RemoteActionLog
(initWebSocketManagers()) -- the log does not exist yet at the point this evaluator
needs to run. Its Verdict is also ALLOWED/DENIED, and "would have been
denied under the old gate but is now unconditionally allowed" is neither.
Logging (#557). Records go to a dedicated non-root Logger with
setUseParentHandlers(false), so a record here never propagates on its own to the root
logger that SystemLogHandler watches (that handler auto-reports any SEVERE record carrying
a Throwable into ErrorReportCollector). A PluginLoggerBridge forwards INFO
and above to the plugin's own logger, so an operator sees the line at its real level and in the
server's normal log format. An earlier private console handler wrote to the standard error stream,
which Paper prints as two WARN lines per record. What an operator sees, by design:
- a clean result (no class would have been refused) is logged at FINE, below what is forwarded, and so is not shown -- the class-name filters no longer exist, so "nothing would have been refused" is not news;
- a non-clean result is one INFO line per module jar, naming the module and the count.
Locale.ROOT, not the default locale. The removed isSafeClassName lowercased with
the no-argument String#toLowerCase(), which uses the JVM's default locale. Under a
Turkish server locale, that method maps an ASCII capital I to a dotless ı, not a
dotted i -- silently changing which class names match a SUSPICIOUS_KEYWORDS
entry depending on where the server happens to run. This evaluator lowercases with
Locale.ROOT instead, so a Turkish-locale server produces the same audit as an
English-locale one. An audit whose result depends on the server's locale is not a measurement,
so that latent defect is corrected here rather than carried into the telemetry meant to replace
the removed enforcement.
- Since:
- 6.3.0
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescription(package private) static enumThe four removed filter layers, in the exact orderisSafeClassNameevaluated them. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate static final LoggerOne accumulator entry per module currently being scanned; reset when its summary emits.(package private) static final LevelThe least severe levelPluginLoggerBridgeforwards to the plugin logger. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescription(package private) static ClassloadFilterAudit.LayerClassifiesclassNameagainst the four removed layers, in the exact orderSecurityPolicy.isSafeClassNameevaluated them before D-12/D-13 (exact blacklist, dangerous package prefix, trusted-package whitelist, suspicious keyword), and returns the FIRST layer that would have refused it -- ornullif none would have.(package private) static voidemitSummary(String moduleName) Emits exactly ONE summary formoduleName: oneLevel.INFOline when at least one class would have been refused by a removed layer, or oneLevel.FINEline when none would (a clean result is the expected case, not operator news).private static boolean(package private) static voidRecords one class evaluated duringmoduleName's scan.
-
Field Details
-
AUDIT_LOGGER
-
FORWARD_LEVEL
The least severe levelPluginLoggerBridgeforwards to the plugin logger. Named, package-private and asserted on byClassloadFilterAuditTestrather than left inline: the live handler list on a JUL logger is global mutable state that other tests in the same JVM add to and remove from, so a test that reads it back is order-dependent. This constant is the decision itself.It is INFO, not ALL, on purpose:
record(java.lang.String, java.lang.String)logs FINE for every classclassify(java.lang.String)returns a layer for, and that is every class of a third-party module (anything outside the seven trusted prefixes), up to a thousand per module. Forwarding those would print up to a thousand lines per module on top of the one INFO summary that is the operator-facing output. The logger itself stays at ALL so a test- or debug-attached handler still receives FINE. -
SYSTEM_DANGEROUS_CLASSES
-
DANGEROUS_PACKAGE_PREFIXES
-
TRUSTED_PACKAGE_PREFIXES
-
SUSPICIOUS_KEYWORDS
-
COUNTS_BY_MODULE
One accumulator entry per module currently being scanned; reset when its summary emits.
-
-
Constructor Details
-
ClassloadFilterAudit
private ClassloadFilterAudit()
-
-
Method Details
-
classify
ClassifiesclassNameagainst the four removed layers, in the exact orderSecurityPolicy.isSafeClassNameevaluated them before D-12/D-13 (exact blacklist, dangerous package prefix, trusted-package whitelist, suspicious keyword), and returns the FIRST layer that would have refused it -- ornullif none would have.A
nullor blankclassNameis not a filter layer and always classifies tonull: the originalisSafeClassNamenull branch is now subsumed byClassLoaderUtils'sVALID_CLASS_NAME_PATTERNformat regex, which is the only thing that still rejects a malformed name (D-13).- Parameters:
className- the fully-qualified class name to classify, ornull/blank- Returns:
- the first layer that would have refused
className, ornull
-
record
Records one class evaluated duringmoduleName's scan. A no-op for anullor blankmoduleName, or whenclassify(String)finds no layer would have refusedclassName-- there is nothing interesting to record for a class none of the four removed layers would have touched.When a layer matches, increments that layer's per-module counter and logs the per-class detail at
Level.FINE-- matchingModuleScanDiagnostics'srecordSkippedClass/emitSummaryshape (D-19), so the two diagnostics read as one pattern rather than two.This method never decides anything: it has no boolean return, and
classNameis still handed to the real classloader by the caller regardless of what this method records (Test 10).- Parameters:
moduleName- the module (or scan unit) currently being evaluatedclassName- the class name being evaluated, ornull/blank
-
emitSummary
Emits exactly ONE summary formoduleName: oneLevel.INFOline when at least one class would have been refused by a removed layer, or oneLevel.FINEline when none would (a clean result is the expected case, not operator news). Resets the accumulator formoduleNameafterward, so a subsequent re-scan of the same module starts clean.This differs deliberately from
ModuleScanDiagnostics.emitSummary, which emits nothing when its accumulator is empty: here a clean module is still a recorded measurement, just not one that is forwarded to the console.- Parameters:
moduleName- the module whose scan just finished
-
isBlank
-