Skip to content

Commit

Permalink
Add new rule set PA_PUBLIC_PRIMITIVE_ATTRIBUTE, PA_PUBLIC_ARRAY_ATTRI…
Browse files Browse the repository at this point in the history
…BUTE and PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE (#1500)

* Add new rule PA_PUBLIC_ATTRIBUTES

SEI CERT rule OBJ01-J requires that accessibility of fields must be
limited. In general, requiring that no fields must be public is
overkill and unrealistic. Even the rule mentions that final fields
may be public, except if the type of the field is a mutable reference
type which can be modified despite the reference itself being final.
Besides final fields, there may be other usages for public fields: some
public fields may serve as "flags" which affect the behavior of the
class. These fields are expected to be read by the containing class
itself and written by other classes. If a field is both written by
the methods of the containing class itself and from outside is
suspicious. This new rule PA_PUBLIC_ATTRIBUTES warns for such field.

* Updated according to the comments of @KengoTODA

* URL to SEI CERT rule changed to a HTML link in messages.xml

* Warnings separated to 3 different messages

* Changed detection of mutable classes and refactored two utility functions

* Constant containers renamed to all-capitals, moved to the front & made static

* Static mutables are detected by MS, duplicate error reports removed

* Missing space inserted into the error message description.

* Grammar fix

* Changelog adjusted

* Detector renamed, bug messages and descriptions changed

* add license header, fix typos

* refactoring tests to the new type

* add sourceline for non-static fields at the first PUT (init)

* making PA_PUBLIC_ATTRIBUTES messages more concrete

* fix tests, whitespaces

* fix CHANGELOG.md to master merge

* Fix Error marker location to the first assignment

* Remove unnecessary line

* CHANGELOG updated

* Small refactor

* Reverting changes to firstFile.xml and secondFile.xml

* spotless apply

---------

Co-authored-by: Ádám Balogh <adam.balogh@ericsson.com>
Co-authored-by: Judit Knoll <judit.knoll@sigmatechnology.com>
Co-authored-by: Judit Knoll <123470644+JuditKnoll@users.noreply.github.com>
  • Loading branch information
4 people committed Jun 9, 2023
1 parent 1ce8b39 commit 64e8c9c
Show file tree
Hide file tree
Showing 7 changed files with 415 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -25,6 +25,7 @@ Currently the versioning policy of this project follows [Semantic Versioning v2.
* New simple name-based AnnotationMatcher for exclude files (now bug annotations store the class java annotations in an attribute called `classAnnotationNames`). For example, use like <Match><Annotation name="org.immutables.value.Generated" /></Match> in an excludeFilter.xml to ignore classes generated by the Immutable framework. This ignores all class, method or field bugs in classes with that annotation.
* Added the Common Weakness Enumeration (CWE) taxonomy to the Static Analysis Results Interchange Format (SARIF) report. The short and long description for the CWEs are retrived from a JSON file which is a slimmed down version of the official comprehensive CWE XML from MITRE. The JSON contains information about all CWEs. ([#2410](https://github.com/spotbugs/spotbugs/pull/2410)).
* New detector `FindAssertionsWithSideEffects` detecting bug `ASSERTION_WITH_SIDE_EFFECT` and `ASSERTION_WITH_SIDE_EFFECT_METHOD` in case of assertions which may have side effects (See [EXP06-J. Expressions used in assertions must not produce side effects](https://wiki.sei.cmu.edu/confluence/display/java/EXP06-J.+Expressions+used+in+assertions+must+not+produce+side+effects))
* New rule set `PA_PUBLIC_PRIMITIVE_ATTRIBUTE`, `PA_PUBLIC_ARRAY_ATTRIBUTE` and `PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE` to warn for public attributes which are written by the methods of the class. This rule is loosely based on the SEI CERT rule *OBJ01-J Limit accessibility of fields*. ([#OBJ01-J](https://wiki.sei.cmu.edu/confluence/display/java/OBJ01-J.+Limit+accessibility+of+fields))

### Security
- Disable access to external entities when processing XML ([#2217](https://github.com/spotbugs/spotbugs/pull/2217))
Expand Down
@@ -0,0 +1,58 @@
package edu.umd.cs.findbugs.detect;

import edu.umd.cs.findbugs.*;
import edu.umd.cs.findbugs.test.matcher.BugInstanceMatcher;
import edu.umd.cs.findbugs.test.matcher.BugInstanceMatcherBuilder;
import org.junit.Test;

import static edu.umd.cs.findbugs.test.CountMatcher.containsExactly;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;

public class FindPublicAttributesTest extends AbstractIntegrationTest {
private static final String PRIMITIVE_PUBLIC = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE";
private static final String MUTABLE_PUBLIC = "PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE";
private static final String ARRAY_PUBLIC = "PA_PUBLIC_ARRAY_ATTRIBUTE";

@Test
public void testPublicAttributesChecks() {
performAnalysis("PublicAttributesTest.class");

assertNumOfBugs(PRIMITIVE_PUBLIC, 3);
assertNumOfBugs(ARRAY_PUBLIC, 4);
assertNumOfBugs(MUTABLE_PUBLIC, 1);

assertBugTypeAtField(PRIMITIVE_PUBLIC, "attr1", 11);
assertBugTypeAtField(PRIMITIVE_PUBLIC, "attr2", 42);
assertBugTypeAtField(PRIMITIVE_PUBLIC, "sattr1", 15);
assertBugTypeAtField(MUTABLE_PUBLIC, "hm", 20);
assertBugTypeAtField(ARRAY_PUBLIC, "items", 28);
assertBugTypeAtField(ARRAY_PUBLIC, "nitems", 48);
assertBugTypeAtField(ARRAY_PUBLIC, "sitems", 32);
assertBugTypeAtField(ARRAY_PUBLIC, "SFITEMS", 34);
}

@Test
public void testGoodPublicAttributesChecks() {
performAnalysis("PublicAttributesNegativeTest.class");
assertNumOfBugs(PRIMITIVE_PUBLIC, 0);
assertNumOfBugs(ARRAY_PUBLIC, 0);
assertNumOfBugs(MUTABLE_PUBLIC, 0);
}

private void assertNumOfBugs(String bugtype, int num) {
final BugInstanceMatcher bugTypeMatcher = new BugInstanceMatcherBuilder()
.bugType(bugtype).build();
assertThat(getBugCollection(), containsExactly(num, bugTypeMatcher));
}

private void assertBugTypeAtField(String bugtype, String field, int line) {
final BugInstanceMatcher bugInstanceMatcher = new BugInstanceMatcherBuilder()
.bugType(bugtype)
.inClass("PublicAttributesTest")
.atField(field)
.atLine(line)
.build();
assertThat(getBugCollection(), hasItem(bugInstanceMatcher));
}
}
7 changes: 6 additions & 1 deletion spotbugs/etc/findbugs.xml
Expand Up @@ -671,7 +671,9 @@
reports="USC_POTENTIAL_SECURITY_CHECK_BASED_ON_UNTRUSTED_SOURCE"/>
<Detector class="edu.umd.cs.findbugs.detect.FindAssertionsWithSideEffects" speed="fast"
reports="ASE_ASSERTION_WITH_SIDE_EFFECT,ASE_ASSERTION_WITH_SIDE_EFFECT_METHOD"/>
<!-- Bug Categories -->
<Detector class="edu.umd.cs.findbugs.detect.FindPublicAttributes" speed="fast"
reports="PA_PUBLIC_PRIMITIVE_ATTRIBUTE,PA_PUBLIC_ARRAY_ATTRIBUTE,PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE"/>
<!-- Bug Categories -->
<BugCategory category="NOISE" hidden="true"/>

<!-- Bug Codes -->
Expand Down Expand Up @@ -1291,4 +1293,7 @@
category="MALICIOUS_CODE"/>
<BugPattern abbrev="ASE" type="ASE_ASSERTION_WITH_SIDE_EFFECT" category="SECURITY" />
<BugPattern abbrev="ASE" type="ASE_ASSERTION_WITH_SIDE_EFFECT_METHOD" category="SECURITY" />
<BugPattern abbrev="PA" type="PA_PUBLIC_PRIMITIVE_ATTRIBUTE" category="BAD_PRACTICE"/>
<BugPattern abbrev="PA" type="PA_PUBLIC_ARRAY_ATTRIBUTE" category="BAD_PRACTICE"/>
<BugPattern abbrev="PA" type="PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE" category="BAD_PRACTICE"/>
</FindbugsPlugin>
72 changes: 72 additions & 0 deletions spotbugs/etc/messages.xml
Expand Up @@ -1779,11 +1779,19 @@ factory pattern to create these objects.</p>
]]>
</Details>
</Detector>
<Detector class="edu.umd.cs.findbugs.detect.FindPublicAttributes">
<Details>
<![CDATA[
<p>This detector looks for public attributes that are also written by the methods of the class.</p>
]]>
</Details>
</Detector>
<!--
**********************************************************************
BugPatterns
**********************************************************************
-->

<BugPattern type="JUA_DONT_ASSERT_INSTANCEOF_IN_TESTS">
<ShortDescription> Asserting value of instanceof in tests is not recommended. </ShortDescription>
<LongDescription> Assertion of type {0} in {2} at {3} may hide useful information about why a cast may have failed.</LongDescription>
Expand Down Expand Up @@ -8820,6 +8828,69 @@ Using floating-point variables should not be used as loop counters, as they are
]]>
</Details>
</BugPattern>
<BugPattern type="PA_PUBLIC_PRIMITIVE_ATTRIBUTE">
<ShortDescription>Primitive field is public</ShortDescription>
<LongDescription>Primitive field {1} is public and set from inside the class, which makes it too exposed. Consider making it private to limit external accessibility.</LongDescription>
<Details>
<![CDATA[
<p>
<a href="https://wiki.sei.cmu.edu/confluence/display/java/OBJ01-J.+Limit+accessibility+of+fields">SEI CERT rule OBJ01-J</a> requires that accessibility to fields must be limited.
Otherwise, the values of the fields may be manipulated from outside the class, which may be unexpected or
undesired behaviour.
In general, requiring that no fields are allowed to be public is overkill and unrealistic. Even
the rule mentions that final fields may be public. Besides final fields, there may be other
usages for public fields: some public fields may serve as "flags" that affect the behavior of
the class. Such flag fields are expected to be read by the current instance (or the current
class, in case of static fields), but written by others. If a field is both written by the
methods of the current instance (or the current class, in case of static fields) and from the
outside, the code is suspicious. Consider making these fields private and provide appropriate
setters, if necessary. Please note that constructors, initializers and finalizers are
exceptions, if only they write the field inside the class, the field is not considered as
written by the class itself.</p>
]]>
</Details>
</BugPattern>
<BugPattern type="PA_PUBLIC_ARRAY_ATTRIBUTE">
<ShortDescription>Array-type field is public</ShortDescription>
<LongDescription>Array-type field {1} is public, which makes it too exposed. Consider making it private to limit external accessibility.</LongDescription>
<Details>
<![CDATA[
<p>
<a href="https://wiki.sei.cmu.edu/confluence/display/java/OBJ01-J.+Limit+accessibility+of+fields">SEI CERT rule OBJ01-J</a> requires that accessibility of fields must be limited.
Making an array-type field final does not prevent other classes from modifying the contents of
the array. However, in general, requiring that no fields are allowed to be public is overkill
and unrealistic. There may be usages for public fields: some public fields may serve as "flags"
that affect the behavior of the class. Such flag fields are expected to be read by the current
instance (or the current class, in case of static fields), but written by others. If a field is
both written by the methods of the current instance (or the current class, in case of static
fields) and from the outside, the code is suspicious. Consider making these fields private and
provide appropriate setters, if necessary. Please note that constructors, initializers and
finalizers are exceptions, if only they write the field inside the class, the field is not
considered as written by the class itself.</p>
]]>
</Details>
</BugPattern>
<BugPattern type="PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE">
<ShortDescription>Mutable object-type field is public</ShortDescription>
<LongDescription>Mutable object-type field {1} is public, which makes it too exposed. Consider making it private to limit external accessibility.</LongDescription>
<Details>
<![CDATA[
<p>
<a href="https://wiki.sei.cmu.edu/confluence/display/java/OBJ01-J.+Limit+accessibility+of+fields">SEI CERT rule OBJ01-J</a> requires that accessibility of fields must be limited.
Making a mutable object-type field final does not prevent other classes from modifying the
contents of the object. However, in general, requiring that no fields are allowed to be public
is overkill and unrealistic. There may be usages for public fields: some public fields may
serve as "flags" that affect the behavior of the class. Such flag fields are expected to be
read by the current instance (or the current class, in case of static fields), but written by
others. If a field is both written by the methods of the current instance (or the current
class, in case of static fields) and from the outside, the code is suspicious. Consider making
these fields private and provide appropriate setters, if necessary. Please note that
constructors, initializers and finalizers are exceptions, if only they write the field inside
the class, the field is not considered as written by the class itself. In case of object-type
fields "writing" means calling methods whose name suggest modification.</p>
]]>
</Details>
</BugPattern>
<!--
**********************************************************************
BugCodes
Expand Down Expand Up @@ -8974,4 +9045,5 @@ Using floating-point variables should not be used as loop counters, as they are
<BugCode abbrev="PERM">Custom class loader does not call its superclass's getPermissions()</BugCode>
<BugCode abbrev="USC">Potential security check based on untrusted source</BugCode>
<BugCode abbrev="ASE">Assertion with side effect</BugCode>
<BugCode abbrev="PA">Public Attribute</BugCode>
</MessageCollection>
@@ -0,0 +1,208 @@
/*
* SpotBugs - Find bugs in Java programs
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

package edu.umd.cs.findbugs.detect;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import edu.umd.cs.findbugs.SourceLineAnnotation;
import org.apache.bcel.Const;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import edu.umd.cs.findbugs.util.MutableClasses;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

public class FindPublicAttributes extends OpcodeStackDetector {

private static final Set<String> CONSTRUCTOR_LIKE_NAMES = new HashSet<String>(Arrays.asList(
Const.CONSTRUCTOR_NAME, Const.STATIC_INITIALIZER_NAME,
"clone", "init", "initialize", "dispose", "finalize", "this",
"_jspInit", "jspDestroy"));

private static final List<String> SETTER_LIKE_NAMES = Arrays.asList(
"set", "put", "add", "insert", "delete", "remove", "erase", "clear",
"push", "pop", "enqueue", "dequeue", "write", "append");

private final BugReporter bugReporter;

private final Set<XField> writtenFields = new HashSet<XField>();

private final Map<XField, SourceLineAnnotation> fieldDefLineMap = new HashMap<XField, SourceLineAnnotation>();

public FindPublicAttributes(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}

@Override
public void sawField() {
XField field = getXFieldOperand();
if (field != null && !fieldDefLineMap.containsKey(field)) {
SourceLineAnnotation sla = SourceLineAnnotation.fromVisitedInstruction(this);
fieldDefLineMap.put(field, sla);
}
}

@Override
public void visit(JavaClass obj) {
for (Method m : obj.getMethods()) {
String methodName = m.getName();
// First visit the Constructor and the static initializer to collect the field initialization lines
if (Const.CONSTRUCTOR_NAME.equals(methodName) || Const.STATIC_INITIALIZER_NAME.equals(methodName)) {
doVisitMethod(m);
}
}
}

// Check for each statement which writes an own attribute of the class
// instance. If the attribute written is public then report a bug.
@Override
public void sawOpcode(int seen) {
// It is normal that classes used as simple data types have a
// constructor to make initialization easy.
if (isConstructorLikeMethod(getMethodName())) {
return;
}

// We do not care for enums and nested classes.
if (getThisClass().isEnum() || getClassName().indexOf('$') >= 0) {
return;
}

if (seen == Const.PUTFIELD || seen == Const.PUTSTATIC) {
XField field = getXFieldOperand();
// Do not report the same public field twice.
if (field == null || writtenFields.contains(field)) {
return;
}

// Only consider attributes of self.
if (!field.getClassDescriptor().equals(getClassDescriptor())) {
return;
}

if (!field.isPublic()) {
return;
}

SourceLineAnnotation sla = fieldDefLineMap.containsKey(field)
? fieldDefLineMap.get(field)
: SourceLineAnnotation.fromVisitedInstruction(this);

bugReporter.reportBug(new BugInstance(this,
"PA_PUBLIC_PRIMITIVE_ATTRIBUTE",
NORMAL_PRIORITY)
.addClass(this).addField(field).addSourceLine(sla));
writtenFields.add(field);
} else if (seen == Const.AASTORE) {
XField field = stack.getStackItem(2).getXField();
// Do not report the same public field twice.
if (field == null || writtenFields.contains(field)) {
return;
}

// Only consider attributes of self.
if (!field.getClassDescriptor().equals(getClassDescriptor())) {
return;
}

if (!field.isPublic()) {
return;
}

SourceLineAnnotation sla = fieldDefLineMap.containsKey(field)
? fieldDefLineMap.get(field)
: SourceLineAnnotation.fromVisitedInstruction(this);

bugReporter.reportBug(new BugInstance(this,
"PA_PUBLIC_ARRAY_ATTRIBUTE",
NORMAL_PRIORITY)
.addClass(this).addField(field).addSourceLine(sla));
writtenFields.add(field);
} else if (seen == Const.INVOKEINTERFACE || seen == Const.INVOKEVIRTUAL) {
XMethod xmo = getXMethodOperand();
if (xmo == null) {
return;
}

// Heuristics: suppose that that model has a proper name in English
// which roughly describes its behavior, beginning with the verb.
// If the verb does not hint that the method writes the object
// then we skip it.
if (!looksLikeASetter(xmo.getName())) {
return;
}

XField field = stack.getStackItem(xmo.getNumParams()).getXField();
// Do not report the same public field twice.
if (field == null || writtenFields.contains(field)) {
return;
}

// Mutable static fields are already detected by MS
if (field.isStatic()) {
return;
}

// Only consider attributes of self.
if (!field.getClassDescriptor().equals(getClassDescriptor())) {
return;
}

if (!field.isPublic() || !field.isReferenceType()) {
return;
}

if (!MutableClasses.mutableSignature(field.getSignature())) {
return;
}

SourceLineAnnotation sla = fieldDefLineMap.containsKey(field)
? fieldDefLineMap.get(field)
: SourceLineAnnotation.fromVisitedInstruction(this);

bugReporter.reportBug(new BugInstance(this,
"PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE",
NORMAL_PRIORITY)
.addClass(this).addField(field).addSourceLine(sla));
writtenFields.add(field);
}
}

private static boolean isConstructorLikeMethod(String methodName) {
return CONSTRUCTOR_LIKE_NAMES.contains(methodName);
}

private static boolean looksLikeASetter(String methodName) {
for (String name : SETTER_LIKE_NAMES) {
if (methodName.startsWith(name)) {
return true;
}
}
return false;
}
}

0 comments on commit 64e8c9c

Please sign in to comment.