Skip to content

Commit 5de593e

Browse files
committed
Prevent finalizer attacks through exception-throwing constructors
1 parent a340e47 commit 5de593e

38 files changed

Lines changed: 1899 additions & 1245 deletions

config/pmd/pmd.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@
2727
<exclude name="GuardLogStatement"/>
2828
</rule>
2929

30+
<rule ref="category/java/bestpractices.xml/UnusedFormalParameter">
31+
<properties>
32+
<property name="violationSuppressXPath">
33+
<value>
34+
.[
35+
parent::FormalParameter
36+
and (@Image = "parametersValidated")
37+
and ancestor::ConstructorDeclaration
38+
]
39+
</value>
40+
</property>
41+
</properties>
42+
</rule>
43+
3044
<rule ref="category/java/codestyle.xml">
3145
<exclude name="AtLeastOneConstructor"/>
3246
<exclude name="CallSuperInConstructor"/>
@@ -263,6 +277,20 @@
263277
</properties>
264278
</rule>
265279

280+
<rule ref="category/java/errorprone.xml/EmptyFinalizer">
281+
<properties>
282+
<!-- false positives, work-around for https://github.com/pmd/pmd/issues/4742 -->
283+
<property name="violationSuppressXPath">
284+
<value>
285+
.[
286+
(../@Final = true())
287+
and (ancestor::TypeDeclaration/@Final = false())
288+
]
289+
</value>
290+
</property>
291+
</properties>
292+
</rule>
293+
266294
<rule ref="category/java/errorprone.xml/NullAssignment">
267295
<properties>
268296
<property name="violationSuppressXPath">

config/spotbugs/spotbugs-exclude.xml

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
3-
~ Copyright 2019-2025 Björn Kautler
3+
~ Copyright 2019-2026 Björn Kautler
44
~
55
~ Licensed under the Apache License, Version 2.0 (the "License");
66
~ you may not use this file except in compliance with the License.
@@ -46,8 +46,14 @@
4646
</Match>
4747

4848
<Match>
49-
<Class name="net.kautler.command.api.CommandHandler"/>
50-
<Method name="ensureInitializationAtStartup" params="java.lang.Object" returns="void"/>
49+
<Or>
50+
<And>
51+
<Class name="net.kautler.command.api.CommandHandler"/>
52+
<Method name="ensureInitializationAtStartup" params="java.lang.Object" returns="void"/>
53+
</And>
54+
<!-- work-around for https://github.com/mebigfatguy/fb-contrib/issues/504 -->
55+
<Method name="finalize" params="" returns="void"/>
56+
</Or>
5157
<Bug pattern="ACEM_ABSTRACT_CLASS_EMPTY_METHODS"/>
5258
</Match>
5359

@@ -62,18 +68,18 @@
6268
<!-- the static string should be clear enough in these cases -->
6369
<And>
6470
<Or>
65-
<Class name="net.kautler.command.api.restriction.javacord.ChannelJavacord"/>
66-
<Class name="net.kautler.command.api.restriction.javacord.RoleJavacord"/>
67-
<Class name="net.kautler.command.api.restriction.javacord.ServerJavacord"/>
68-
<Class name="net.kautler.command.api.restriction.javacord.UserJavacord"/>
69-
<Class name="net.kautler.command.api.restriction.javacord.slash.ChannelJavacordSlash"/>
70-
<Class name="net.kautler.command.api.restriction.javacord.slash.RoleJavacordSlash"/>
71-
<Class name="net.kautler.command.api.restriction.javacord.slash.ServerJavacordSlash"/>
72-
<Class name="net.kautler.command.api.restriction.javacord.slash.UserJavacordSlash"/>
73-
<Class name="net.kautler.command.api.restriction.jda.ChannelJda"/>
74-
<Class name="net.kautler.command.api.restriction.jda.GuildJda"/>
75-
<Class name="net.kautler.command.api.restriction.jda.RoleJda"/>
76-
<Class name="net.kautler.command.api.restriction.jda.UserJda"/>
71+
<Class name="net.kautler.command.api.restriction.javacord.ChannelJavacord$Parameters"/>
72+
<Class name="net.kautler.command.api.restriction.javacord.RoleJavacord$Parameters"/>
73+
<Class name="net.kautler.command.api.restriction.javacord.ServerJavacord$Parameters"/>
74+
<Class name="net.kautler.command.api.restriction.javacord.UserJavacord$Parameters"/>
75+
<Class name="net.kautler.command.api.restriction.javacord.slash.ChannelJavacordSlash$Parameters"/>
76+
<Class name="net.kautler.command.api.restriction.javacord.slash.RoleJavacordSlash$Parameters"/>
77+
<Class name="net.kautler.command.api.restriction.javacord.slash.ServerJavacordSlash$Parameters"/>
78+
<Class name="net.kautler.command.api.restriction.javacord.slash.UserJavacordSlash$Parameters"/>
79+
<Class name="net.kautler.command.api.restriction.jda.ChannelJda$Parameters"/>
80+
<Class name="net.kautler.command.api.restriction.jda.GuildJda$Parameters"/>
81+
<Class name="net.kautler.command.api.restriction.jda.RoleJda$Parameters"/>
82+
<Class name="net.kautler.command.api.restriction.jda.UserJda$Parameters"/>
7783
</Or>
7884
<Or>
7985
<Method name="ensureAtLeastOneConditionIsSet" params="" returns="void"/>
@@ -123,11 +129,6 @@
123129
<Method name="getAdditionalData" params="java.lang.String, java.lang.Object" returns="java.lang.Object"/>
124130
</Or>
125131
</And>
126-
<!-- currently not used outside the class, but should be available for usage -->
127-
<And>
128-
<Class name="net.kautler.test.pitest.ExplicitMutationFilterDetails"/>
129-
<Method name="&lt;init&gt;" params="int, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String" returns="void"/>
130-
</And>
131132
</Or>
132133
<Bug pattern="OPM_OVERLY_PERMISSIVE_METHOD"/>
133134
</Match>

gradle/build-logic/src/main/kotlin/net/kautler/tests.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,18 @@ tasks.withType<JacocoReport>().configureEach {
341341
}
342342

343343
tasks.jacocoTestCoverageVerification {
344+
classDirectories = files(
345+
classDirectories
346+
.files
347+
.map {
348+
fileTree(it) {
349+
// it is impossible to cover this class due to its nature, so exclude
350+
// it from reporting to be able to check for 100% coverage otherwise
351+
exclude("net/kautler/command/parameter/parser/missingdependency/MissingDependencyParameterParser.class")
352+
}
353+
}
354+
)
355+
344356
violationRules {
345357
rule {
346358
element = "CLASS"

readme/README_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ License
804804
-------
805805

806806
```plain
807-
Copyright 2019-2023 Björn Kautler
807+
Copyright 2019-2026 Björn Kautler
808808
809809
Licensed under the Apache License, Version 2.0 (the "License");
810810
you may not use this file except in compliance with the License.

src/main/java/net/kautler/command/api/CommandContext.java

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Björn Kautler
2+
* Copyright 2022-2025 Björn Kautler
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -74,18 +74,40 @@ public class CommandContext<M> {
7474
/**
7575
* Constructs a new command context from the given builder.
7676
*
77-
* @param builder the message that triggered the command processing
77+
* @param builder the builder holding the configuration for the new command context
7878
*/
7979
private CommandContext(Builder<M> builder) {
80-
message = requireNonNull(builder.message);
81-
messageContent = requireNonNull(builder.messageContent);
80+
this(true, requireNonEmptyMessageAndContent(builder));
81+
}
82+
83+
/**
84+
* Constructs a new command context from the given builder.
85+
*
86+
* @param parametersValidated a dummy parameter for finalizer attack prevention
87+
* @param builder the builder holding the configuration for the new command context
88+
*/
89+
private CommandContext(boolean parametersValidated, Builder<M> builder) {
90+
message = builder.message;
91+
messageContent = builder.messageContent;
8292
prefix = builder.prefix;
8393
alias = builder.alias;
8494
parameterString = builder.parameterString;
8595
command = builder.command;
8696
additionalData.putAll(builder.additionalData);
8797
}
8898

99+
/**
100+
* Ensures that the given builder has a non-null message and message content.
101+
*
102+
* @param builder the builder to be validated
103+
* @return the validated builder
104+
*/
105+
private static <M> Builder<M> requireNonEmptyMessageAndContent(Builder<M> builder) {
106+
requireNonNull(builder.message);
107+
requireNonNull(builder.messageContent);
108+
return builder;
109+
}
110+
89111
/**
90112
* Returns the message that triggered the command processing.
91113
*
@@ -436,8 +458,19 @@ public static class Builder<M> {
436458
* @param messageContent the content of the message that triggered the command processing
437459
*/
438460
public Builder(M message, String messageContent) {
439-
this.message = requireNonNull(message);
440-
this.messageContent = requireNonNull(messageContent);
461+
this(true, requireNonNull(message), requireNonNull(messageContent));
462+
}
463+
464+
/**
465+
* Constructs a new command context builder with the given message and message content.
466+
*
467+
* @param parametersValidated a dummy parameter for finalizer attack prevention
468+
* @param message the message that triggered the command processing
469+
* @param messageContent the content of the message that triggered the command processing
470+
*/
471+
private Builder(boolean parametersValidated, M message, String messageContent) {
472+
this.message = message;
473+
this.messageContent = messageContent;
441474
}
442475

443476
/**
@@ -446,15 +479,38 @@ public Builder(M message, String messageContent) {
446479
* @param commandContext the command context used to initialize the builder
447480
*/
448481
private Builder(CommandContext<M> commandContext) {
449-
message = requireNonNull(commandContext.message);
450-
messageContent = requireNonNull(commandContext.messageContent);
482+
this(true, requireNonEmptyMessageAndContentAndAdditionalData(commandContext));
483+
}
484+
485+
/**
486+
* Constructs a new command context builder with the same values as the given command context.
487+
*
488+
* @param parametersValidated a dummy parameter for finalizer attack prevention
489+
* @param commandContext the command context used to initialize the builder
490+
*/
491+
private Builder(boolean parametersValidated, CommandContext<M> commandContext) {
492+
message = commandContext.message;
493+
messageContent = commandContext.messageContent;
451494
prefix = commandContext.prefix;
452495
alias = commandContext.alias;
453496
parameterString = commandContext.parameterString;
454497
command = commandContext.command;
498+
additionalData.putAll(commandContext.additionalData);
499+
}
500+
501+
/**
502+
* Ensures that the given command context has a non-null message and message content, and that all
503+
* additional data keys and values are non-null.
504+
*
505+
* @param commandContext the command context to be validated
506+
* @return the validated command context
507+
*/
508+
private static <M> CommandContext<M> requireNonEmptyMessageAndContentAndAdditionalData(CommandContext<M> commandContext) {
509+
requireNonNull(commandContext.message);
510+
requireNonNull(commandContext.messageContent);
455511
commandContext.additionalData.keySet().forEach(Objects::requireNonNull);
456512
commandContext.additionalData.values().forEach(Objects::requireNonNull);
457-
additionalData.putAll(commandContext.additionalData);
513+
return commandContext;
458514
}
459515

460516
/**

src/main/java/net/kautler/command/api/restriction/RestrictionChainElement.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2022 Björn Kautler
2+
* Copyright 2019-2025 Björn Kautler
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -48,7 +48,17 @@ public class RestrictionChainElement {
4848
* @param restriction the restriction to wrap
4949
*/
5050
public RestrictionChainElement(Class<? extends Restriction<?>> restriction) {
51-
this.restriction = requireNonNull(restriction);
51+
this(true, requireNonNull(restriction));
52+
}
53+
54+
/**
55+
* Constructs a new restriction chain element for the given restriction class.
56+
*
57+
* @param parametersValidated a dummy parameter for finalizer attack prevention
58+
* @param restriction the restriction to wrap
59+
*/
60+
private RestrictionChainElement(boolean parametersValidated, Class<? extends Restriction<?>> restriction) {
61+
this.restriction = restriction;
5262
}
5363

5464
/**

0 commit comments

Comments
 (0)