Skip to content

Commit c1299e0

Browse files
authored
Merge pull request #395 from newrelic/task/deserialisation_improvement_poc
Deserialisation Event Generation : vulnerability detection
2 parents 9f1d5eb + d78ca37 commit c1299e0

File tree

33 files changed

+1365
-95
lines changed

33 files changed

+1365
-95
lines changed

Changelog.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ Noteworthy changes to the agent are documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.7.0] - TBD
8+
### Adds
9+
- [PR-395](https://github.com/newrelic/csec-java-agent/pull/395) **Support for Deserialization Vulnerability Detection**: Implemented mechanisms to detect vulnerabilities arising from unsafe deserialization processes.
10+
- [PR-395](https://github.com/newrelic/csec-java-agent/pull/395) **Support for Vulnerability Detection of Remote Code Invocation via Reflection**: Enhanced capability to identify security risks associated with remote code execution through reflection.
11+
- [PR-343](https://github.com/newrelic/csec-java-agent/pull/343) **HTTP Response Handling for Vulnerabilities**: Developed the functionality to send HTTP responses for detected vulnerabilities directly to the UI.
12+
13+
### Changes
14+
- [PR-343](https://github.com/newrelic/csec-java-agent/pull/343) **Trimmed Response Body**: Updated the response handling logic to trim response bodies to a maximum of 500KB when larger. This optimization aids in performance and resource conservation.
15+
- [PR-396](https://github.com/newrelic/csec-java-agent/pull/396) Upgraded _commons-io:commons-io_ from version 2.7 to 2.14.0
16+
- [PR-403](https://github.com/newrelic/csec-java-agent/pull/403) GraphQL Supported Version Range: Restricted the supported version range for GraphQL due to the release of a new version on April 7th, 2025
17+
18+
### Fixes
19+
- [PR-372](https://github.com/newrelic/csec-java-agent/pull/372) **Repeat IAST Request Relay Commands**: Reconfigured logic to repeat IAST control commands until the endpoint is confirmed.
20+
21+
722
## [1.6.1] - 2025-3-1
823
### Adds
924
- [PR-309](https://github.com/newrelic/csec-java-agent/pull/309) Introduced API Endpoint detection for Resin Server. [NR-293077](https://new-relic.atlassian.net/browse/NR-293077)
@@ -17,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1732
- [PR-364](https://github.com/newrelic/csec-java-agent/pull/364) Modified HealthCheck to include the iastTestIdentifier and adjusted WebSocket headers to send instance-count only when its value is greater than zero. [NR-347851](https://new-relic.atlassian.net/browse/NR-347851)
1833
- [PR-349](https://github.com/newrelic/csec-java-agent/pull/349) Enhanced the process for rolling over log files, allowing for specific prefixes and suffixes. [NR-337016](https://new-relic.atlassian.net/browse/NR-337016)
1934

35+
2036
## [1.6.0] - 2024-12-16
2137
### Adds
2238
- [PR-329](https://github.com/newrelic/csec-java-agent/pull/329) Apache Pekko Server Support: The security agent now supports Apache Pekko Server version 1.0.0 and newer, compatible with Scala 2.13 and above. [NR-308780](https://new-relic.atlassian.net/browse/NR-308780), [NR-308781](https://new-relic.atlassian.net/browse/NR-308781), [NR-308791](https://new-relic.atlassian.net/browse/NR-308791), [NR-308792](https://new-relic.atlassian.net/browse/NR-308792) [NR-308782](https://new-relic.atlassian.net/browse/NR-308782)

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The agent version.
22
agentVersion=1.6.1
3-
jsonVersion=1.2.10
3+
jsonVersion=1.2.11
44
# Updated exposed NR APM API version.
55
nrAPIVersion=8.12.0
66

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
dependencies {
2+
implementation(project(":newrelic-security-api"))
3+
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
4+
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
5+
}
6+
7+
// This instrumentation module should not use the bootstrap classpath
8+
9+
10+
jar {
11+
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.deserialisation' }
12+
}
13+
14+
verifyInstrumentation {
15+
verifyClasspath = false // We don't want to verify classpath since these are JDK classes
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package java.io;
2+
3+
public class ObjectInputStreamHelper {
4+
public static final String METHOD_NAME_READ_OBJECT = "readObject";
5+
6+
public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "UNSAFE-DESERIALISATION-LOCK-JAVA-IO-%s-";
7+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package java.io;
2+
3+
import com.newrelic.api.agent.security.NewRelicSecurity;
4+
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
5+
import com.newrelic.api.agent.security.schema.*;
6+
import com.newrelic.api.agent.security.schema.Serializable;
7+
import com.newrelic.api.agent.security.schema.operation.DeserializationOperation;
8+
import com.newrelic.api.agent.weaver.MatchType;
9+
import com.newrelic.api.agent.weaver.Weave;
10+
import com.newrelic.api.agent.weaver.Weaver;
11+
12+
import java.util.Arrays;
13+
14+
15+
@Weave(type = MatchType.BaseClass, originalName = "java.io.ObjectInputStream")
16+
public abstract class ObjectInputStream_Instrumentation {
17+
18+
private void readSerialData(Object obj, ObjectStreamClass desc)
19+
throws IOException {
20+
if(NewRelicSecurity.isHookProcessingActive()) {
21+
DeserializationInfo dInfo = preProcessSecurityHook(obj);
22+
}
23+
Weaver.callOriginal();
24+
}
25+
26+
private void filterCheck(Class<?> clazz, int arrayLength)
27+
throws InvalidClassException {
28+
boolean isLockAcquired = acquireLockIfPossible("filterCheck");
29+
boolean filterCheck = false;
30+
try {
31+
Weaver.callOriginal();
32+
filterCheck = true;
33+
} finally {
34+
if(isLockAcquired) {
35+
processFilterCheck(clazz, filterCheck);
36+
GenericHelper.releaseLock(String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, "filterCheck"));
37+
}
38+
}
39+
}
40+
41+
protected Class<?> resolveClass(ObjectStreamClass desc)
42+
throws IOException, ClassNotFoundException
43+
{
44+
boolean isLockAcquired = acquireLockIfPossible("resolve");
45+
Class<?> returnValue = null;
46+
try {
47+
returnValue = Weaver.callOriginal();
48+
} finally {
49+
if(isLockAcquired) {
50+
processResolveClass(desc, returnValue);
51+
GenericHelper.releaseLock(String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, "resolve"));
52+
}
53+
}
54+
return returnValue;
55+
}
56+
57+
private DeserializationInfo preProcessSecurityHook(Object obj) {
58+
DeserializationInfo dInfo = new DeserializationInfo(obj.getClass().getName(), obj);
59+
NewRelicSecurity.getAgent().getSecurityMetaData().addToDeserializationRoot(dInfo);
60+
return dInfo;
61+
}
62+
63+
64+
private final Object readObject(Class<?> type)
65+
throws IOException, ClassNotFoundException {
66+
boolean isLockAcquired = acquireLockIfPossible("readObject");
67+
DeserializationInvocation deserializationInvocation = null;
68+
DeserializationOperation operation = null;
69+
70+
if(isLockAcquired) {
71+
operation = new DeserializationOperation(
72+
this.getClass().getName(),
73+
ObjectInputStreamHelper.METHOD_NAME_READ_OBJECT
74+
);
75+
deserializationInvocation = new DeserializationInvocation(true, operation.getExecutionId());
76+
NewRelicSecurity.getAgent().getSecurityMetaData().setDeserializationInvocation(deserializationInvocation);
77+
operation.setDeserializationInvocation(deserializationInvocation);
78+
// NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(InstrumentationConstants.ACTIVE_DESERIALIZATION, true);
79+
}
80+
try {
81+
return Weaver.callOriginal();
82+
} finally {
83+
if(isLockAcquired) {
84+
if(NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot() != null) {
85+
operation.setRootDeserializationInfo(NewRelicSecurity.getAgent().getSecurityMetaData()
86+
.peekDeserializationRoot());
87+
operation.setEntityName(operation.getRootDeserializationInfo().getType());
88+
}
89+
NewRelicSecurity.getAgent().registerOperation(operation);
90+
NewRelicSecurity.getAgent().getSecurityMetaData().setDeserializationInvocation(null);
91+
NewRelicSecurity.getAgent().getSecurityMetaData().resetDeserializationRoot();
92+
GenericHelper.releaseLock(String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, "readObject"));
93+
}
94+
}
95+
}
96+
97+
private void processFilterCheck(Class<?> clazz, boolean filterCheck) {
98+
99+
DeserializationInvocation deserializationInvocation = NewRelicSecurity.getAgent().getSecurityMetaData().getDeserializationInvocation();
100+
if(deserializationInvocation != null && clazz != null) {
101+
com.newrelic.api.agent.security.schema.Serializable serializable = deserializationInvocation.getEncounteredSerializableByName(clazz.getName());
102+
if(serializable == null) {
103+
serializable = new Serializable(clazz.getName(), true);
104+
serializable.setKlass(clazz);
105+
deserializationInvocation.addEncounteredSerializable(serializable);
106+
// serializable.setClassDefinition(getClassDefinition(ObjectStreamClass.lookup(clazz)));
107+
}
108+
if(!filterCheck) {
109+
serializable.setDeserializable(false);
110+
}
111+
}
112+
}
113+
114+
private void processResolveClass(ObjectStreamClass desc, Class<?> returnValue) {
115+
DeserializationInvocation deserializationInvocation = NewRelicSecurity.getAgent().getSecurityMetaData().getDeserializationInvocation();
116+
if(deserializationInvocation != null) {
117+
Serializable serializable = deserializationInvocation.getEncounteredSerializableByName(desc.getName());
118+
if(serializable == null) {
119+
serializable = new Serializable(desc.getName(), true);
120+
serializable.setKlass(returnValue);
121+
deserializationInvocation.addEncounteredSerializable(serializable);
122+
// serializable.setClassDefinition(getClassDefinition(desc));
123+
}
124+
if(returnValue == null) {
125+
serializable.setDeserializable(false);
126+
}
127+
}
128+
}
129+
130+
private boolean acquireLockIfPossible(String operation) {
131+
return GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.UNSAFE_DESERIALIZATION, String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, operation));
132+
}
133+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package java.io;
2+
3+
import com.newrelic.api.agent.security.NewRelicSecurity;
4+
import com.newrelic.api.agent.security.schema.DeserializationInvocation;
5+
import com.newrelic.api.agent.weaver.MatchType;
6+
import com.newrelic.api.agent.weaver.Weave;
7+
import com.newrelic.api.agent.weaver.Weaver;
8+
9+
@Weave(type = MatchType.BaseClass, originalName = "java.io.ObjectStreamClass")
10+
public class ObjectStreamClass_Instrumentation {
11+
12+
void invokeReadObject(Object obj, ObjectInputStream in)
13+
throws ClassNotFoundException, IOException,
14+
UnsupportedOperationException
15+
{
16+
if(NewRelicSecurity.isHookProcessingActive()) {
17+
DeserializationInvocation deserializationInvocation = NewRelicSecurity.getAgent().getSecurityMetaData().getDeserializationInvocation();
18+
if (deserializationInvocation != null) {
19+
deserializationInvocation.pushReadObjectInAction(obj.getClass().getName());
20+
}
21+
Weaver.callOriginal();
22+
if (deserializationInvocation != null) {
23+
deserializationInvocation.popReadObjectInAction();
24+
}
25+
}
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
dependencies {
2+
implementation(project(":newrelic-security-api"))
3+
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
4+
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
5+
}
6+
7+
// This instrumentation module should not use the bootstrap classpath
8+
9+
10+
jar {
11+
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.java-reflection' }
12+
}
13+
14+
verifyInstrumentation {
15+
verifyClasspath = false // We don't want to verify classpath since these are JDK classes
16+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package java.lang.reflect;
2+
3+
import com.newrelic.api.agent.security.NewRelicSecurity;
4+
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
5+
import com.newrelic.api.agent.security.schema.AbstractOperation;
6+
import com.newrelic.api.agent.security.schema.StringUtils;
7+
import com.newrelic.api.agent.security.schema.VulnerabilityCaseType;
8+
import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException;
9+
import com.newrelic.api.agent.security.schema.operation.JavaReflectionOperation;
10+
import com.newrelic.api.agent.security.utils.logging.LogLevel;
11+
import com.newrelic.api.agent.weaver.MatchType;
12+
import com.newrelic.api.agent.weaver.Weave;
13+
import com.newrelic.api.agent.weaver.Weaver;
14+
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
19+
@Weave(type = MatchType.ExactClass, originalName = "java.lang.reflect.Method")
20+
public abstract class Method_Instrumentation {
21+
22+
public abstract String getName();
23+
24+
public abstract Class<?> getDeclaringClass();
25+
26+
public abstract Class<?>[] getParameterTypes();
27+
28+
public Object invoke(Object obj, Object... args) {
29+
AbstractOperation operation = null;
30+
if(NewRelicSecurity.isHookProcessingActive()) {
31+
operation = preprocessSecurityHook(obj, getDeclaringClass(), getParameterTypes(), getName(), args);
32+
}
33+
Object returnValue = Weaver.callOriginal();
34+
registerExitOperation(operation);
35+
return returnValue;
36+
}
37+
38+
private void registerExitOperation(AbstractOperation operation) {
39+
if (operation == null || !NewRelicSecurity.isHookProcessingActive() ||
40+
NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || GenericHelper.skipExistsEvent()
41+
) {
42+
return;
43+
}
44+
NewRelicSecurity.getAgent().registerExitEvent(operation);
45+
}
46+
47+
private AbstractOperation preprocessSecurityHook(Object obj, Class<?> declaringClass, Class<?>[] parameterTypes, String name, Object[] args) {
48+
try {
49+
if(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || !GenericHelper.isLockAcquirePossible(VulnerabilityCaseType.REFLECTION)) {
50+
return null;
51+
}
52+
53+
JavaReflectionOperation operation = new JavaReflectionOperation(this.getClass().getName(), "invoke", declaringClass.getName(), name, args, obj);
54+
List<String> methodNames = new ArrayList<>();
55+
for (Method method : declaringClass.getDeclaredMethods()) {
56+
if(Arrays.equals(method.getParameterTypes(), parameterTypes)) {
57+
methodNames.add(method.getName());
58+
}
59+
}
60+
operation.setDeclaredMethods(methodNames);
61+
NewRelicSecurity.getAgent().registerOperation(operation);
62+
return operation;
63+
} catch (Throwable e) {
64+
if(e instanceof NewRelicSecurityException){
65+
NewRelicSecurity.getAgent().log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, "JAVA-REFLECTION", e.getMessage()), e, Method_Instrumentation.class.getName());
66+
throw e;
67+
}
68+
NewRelicSecurity.getAgent().log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, "JAVA-REFLECTION", e.getMessage()), e, Method_Instrumentation.class.getName());
69+
NewRelicSecurity.getAgent().reportIncident(LogLevel.SEVERE , String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, "JAVA-REFLECTION", e.getMessage()), e, Method_Instrumentation.class.getName());
70+
}
71+
return null;
72+
}
73+
}

newrelic-security-agent/src/main/java/com/newrelic/agent/security/AgentConfig.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ private void readSkipScan() throws RestrictionModeException {
193193
agentMode.getSkipScan().getIastDetectionCategory().setXpathInjectionEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_XPATH_INJECTION, false));
194194
agentMode.getSkipScan().getIastDetectionCategory().setSsrfEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_SSRF, false));
195195
agentMode.getSkipScan().getIastDetectionCategory().setRxssEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_RXSS, false));
196+
agentMode.getSkipScan().getIastDetectionCategory().setUnsafeDeserializationEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_UNSAFE_DESERIALIZATION, false));
197+
agentMode.getSkipScan().getIastDetectionCategory().setInsecureReflectionEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_UNSAFE_REFLECTION, false));
196198
if(!agentMode.getSkipScan().getIastDetectionCategory().getRxssEnabled() && !NewRelic.getAgent().getConfig().getValue(REPORT_HTTP_RESPONSE_BODY, true)) {
197199
agentMode.getSkipScan().getIastDetectionCategory().setRxssEnabled(true);
198200
}
@@ -316,7 +318,7 @@ private String applyRequiredLogLevel() {
316318
if(value instanceof Boolean) {
317319
logLevel = IUtilConstants.OFF;
318320
} else {
319-
logLevel = NewRelic.getAgent().getConfig().getValue(IUtilConstants.NR_LOG_LEVEL, IUtilConstants.INFO);
321+
logLevel = NewRelic.getAgent().getConfig().getValue(IUtilConstants.NR_LOG_LEVEL, LogLevel.FINEST.name());
320322
}
321323

322324
try {

0 commit comments

Comments
 (0)