Skip to content

Deserialisation Event Generation : vulnerability detection #395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7bd7d3b
Initial changes for proof backed deserialization attack detection
AnupamJuniwal Sep 6, 2023
a8bff23
Merge branch 'develop' into task/deserialisation_improvement_poc
AnupamJuniwal Sep 7, 2023
cac032e
This contains code cleanup and multiple fixes as required to gather m…
AnupamJuniwal Sep 13, 2023
81345c0
Merge branch 'develop' into task/deserialisation_improvement_poc
AnupamJuniwal Sep 26, 2023
9fff3b7
Code cleanup
AnupamJuniwal Sep 26, 2023
c17d614
This contains changes for deserializationInfo to be managed with one …
AnupamJuniwal Oct 18, 2023
58fa096
minor fix for NPE handling
AnupamJuniwal Oct 30, 2023
f58a5c8
Merge branch 'develop' into task/deserialisation_improvement_poc
AnupamJuniwal Nov 21, 2023
4cd631f
Refactoring for java deserialization hook
AnupamJuniwal Nov 27, 2023
ee8d3f3
Added todo note for object deserialiation
AnupamJuniwal Jan 13, 2025
93d10f0
Merge branch 'refs/heads/main' into task/deserialisation_improvement_poc
lovesh-ap Jan 13, 2025
12559be
Deserialization POC without reflection
lovesh-ap Jan 15, 2025
1e115e0
Merge branch 'refs/heads/main' into task/deserialisation_improvement_poc
lovesh-ap Feb 4, 2025
d72a9ac
change parameter schema for Deserialization event
lovesh-ap Feb 7, 2025
bfb2acd
Bump json version to 1.2.11
IshikaDawda Feb 11, 2025
fd3d572
Prodcutisation of DeserializationInfo
lovesh-ap Feb 24, 2025
9ef161e
Merge branch 'refs/heads/feature/include-http-response' into task/des…
lovesh-ap Feb 25, 2025
4577300
Add UNSAFE_DESERIALIZATION as a new category
lovesh-ap Feb 26, 2025
90f0324
Introduce Deserialization Vulnerability detection and
lovesh-ap Apr 11, 2025
6540e78
Deserialization event size reduction
lovesh-ap Apr 11, 2025
f077121
update json version to 1.2.11
lovesh-ap Apr 11, 2025
8e00a41
Remove deserialization context and add category reflection to Skip Scan
lovesh-ap Apr 14, 2025
27391e2
Remove hard check on deserialization value
lovesh-ap Apr 15, 2025
ef528f6
Remove unused Class and sysouts
lovesh-ap Apr 15, 2025
381d49a
Generate all reflection events
lovesh-ap Apr 16, 2025
aaa48a6
Log level change for debugging
lovesh-ap Apr 21, 2025
d1cf30b
Changelogs for 1.7.0 release
lovesh-ap Apr 21, 2025
d78ca37
Merge branch 'refs/heads/feature/include-http-response' into task/des…
IshikaDawda Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ Noteworthy changes to the agent are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.7.0] - TBD
### Adds
- [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.
- [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.
- [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.

### Changes
- [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.
- [PR-396](https://github.com/newrelic/csec-java-agent/pull/396) Upgraded _commons-io:commons-io_ from version 2.7 to 2.14.0
- [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

### Fixes
- [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.


## [1.6.1] - 2025-3-1
### Adds
- [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)
Expand All @@ -17,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [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)
- [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)


## [1.6.0] - 2024-12-16
### Adds
- [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)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The agent version.
agentVersion=1.6.1
jsonVersion=1.2.10
jsonVersion=1.2.11
# Updated exposed NR APM API version.
nrAPIVersion=8.12.0

Expand Down
16 changes: 16 additions & 0 deletions instrumentation-security/deserialisation/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
}

// This instrumentation module should not use the bootstrap classpath


jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.deserialisation' }
}

verifyInstrumentation {
verifyClasspath = false // We don't want to verify classpath since these are JDK classes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package java.io;

public class ObjectInputStreamHelper {
public static final String METHOD_NAME_READ_OBJECT = "readObject";

public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "UNSAFE-DESERIALISATION-LOCK-JAVA-IO-%s-";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package java.io;

import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
import com.newrelic.api.agent.security.schema.*;
import com.newrelic.api.agent.security.schema.Serializable;
import com.newrelic.api.agent.security.schema.operation.DeserializationOperation;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

import java.util.Arrays;


@Weave(type = MatchType.BaseClass, originalName = "java.io.ObjectInputStream")
public abstract class ObjectInputStream_Instrumentation {

private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException {
if(NewRelicSecurity.isHookProcessingActive()) {
DeserializationInfo dInfo = preProcessSecurityHook(obj);
}
Weaver.callOriginal();
}

private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
boolean isLockAcquired = acquireLockIfPossible("filterCheck");
boolean filterCheck = false;
try {
Weaver.callOriginal();
filterCheck = true;
} finally {
if(isLockAcquired) {
processFilterCheck(clazz, filterCheck);
GenericHelper.releaseLock(String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, "filterCheck"));
}
}
}

protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
boolean isLockAcquired = acquireLockIfPossible("resolve");
Class<?> returnValue = null;
try {
returnValue = Weaver.callOriginal();
} finally {
if(isLockAcquired) {
processResolveClass(desc, returnValue);
GenericHelper.releaseLock(String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, "resolve"));
}
}
return returnValue;
}

private DeserializationInfo preProcessSecurityHook(Object obj) {
DeserializationInfo dInfo = new DeserializationInfo(obj.getClass().getName(), obj);
NewRelicSecurity.getAgent().getSecurityMetaData().addToDeserializationRoot(dInfo);
return dInfo;
}


private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException {
boolean isLockAcquired = acquireLockIfPossible("readObject");
DeserializationInvocation deserializationInvocation = null;
DeserializationOperation operation = null;

if(isLockAcquired) {
operation = new DeserializationOperation(
this.getClass().getName(),
ObjectInputStreamHelper.METHOD_NAME_READ_OBJECT
);
deserializationInvocation = new DeserializationInvocation(true, operation.getExecutionId());
NewRelicSecurity.getAgent().getSecurityMetaData().setDeserializationInvocation(deserializationInvocation);
operation.setDeserializationInvocation(deserializationInvocation);
// NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(InstrumentationConstants.ACTIVE_DESERIALIZATION, true);
}
try {
return Weaver.callOriginal();
} finally {
if(isLockAcquired) {
if(NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot() != null) {
operation.setRootDeserializationInfo(NewRelicSecurity.getAgent().getSecurityMetaData()
.peekDeserializationRoot());
operation.setEntityName(operation.getRootDeserializationInfo().getType());
}
NewRelicSecurity.getAgent().registerOperation(operation);
NewRelicSecurity.getAgent().getSecurityMetaData().setDeserializationInvocation(null);
NewRelicSecurity.getAgent().getSecurityMetaData().resetDeserializationRoot();
GenericHelper.releaseLock(String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, "readObject"));
}
}
}

private void processFilterCheck(Class<?> clazz, boolean filterCheck) {

DeserializationInvocation deserializationInvocation = NewRelicSecurity.getAgent().getSecurityMetaData().getDeserializationInvocation();
if(deserializationInvocation != null && clazz != null) {
com.newrelic.api.agent.security.schema.Serializable serializable = deserializationInvocation.getEncounteredSerializableByName(clazz.getName());
if(serializable == null) {
serializable = new Serializable(clazz.getName(), true);
serializable.setKlass(clazz);
deserializationInvocation.addEncounteredSerializable(serializable);
// serializable.setClassDefinition(getClassDefinition(ObjectStreamClass.lookup(clazz)));
}
if(!filterCheck) {
serializable.setDeserializable(false);
}
}
}

private void processResolveClass(ObjectStreamClass desc, Class<?> returnValue) {
DeserializationInvocation deserializationInvocation = NewRelicSecurity.getAgent().getSecurityMetaData().getDeserializationInvocation();
if(deserializationInvocation != null) {
Serializable serializable = deserializationInvocation.getEncounteredSerializableByName(desc.getName());
if(serializable == null) {
serializable = new Serializable(desc.getName(), true);
serializable.setKlass(returnValue);
deserializationInvocation.addEncounteredSerializable(serializable);
// serializable.setClassDefinition(getClassDefinition(desc));
}
if(returnValue == null) {
serializable.setDeserializable(false);
}
}
}

private boolean acquireLockIfPossible(String operation) {
return GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.UNSAFE_DESERIALIZATION, String.format(ObjectInputStreamHelper.NR_SEC_CUSTOM_ATTRIB_NAME, operation));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package java.io;

import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.schema.DeserializationInvocation;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

@Weave(type = MatchType.BaseClass, originalName = "java.io.ObjectStreamClass")
public class ObjectStreamClass_Instrumentation {

void invokeReadObject(Object obj, ObjectInputStream in)
throws ClassNotFoundException, IOException,
UnsupportedOperationException
{
if(NewRelicSecurity.isHookProcessingActive()) {
DeserializationInvocation deserializationInvocation = NewRelicSecurity.getAgent().getSecurityMetaData().getDeserializationInvocation();
if (deserializationInvocation != null) {
deserializationInvocation.pushReadObjectInAction(obj.getClass().getName());
}
Weaver.callOriginal();
if (deserializationInvocation != null) {
deserializationInvocation.popReadObjectInAction();
}
}
}
}
16 changes: 16 additions & 0 deletions instrumentation-security/java-reflection/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
}

// This instrumentation module should not use the bootstrap classpath


jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.java-reflection' }
}

verifyInstrumentation {
verifyClasspath = false // We don't want to verify classpath since these are JDK classes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package java.lang.reflect;

import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
import com.newrelic.api.agent.security.schema.AbstractOperation;
import com.newrelic.api.agent.security.schema.StringUtils;
import com.newrelic.api.agent.security.schema.VulnerabilityCaseType;
import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException;
import com.newrelic.api.agent.security.schema.operation.JavaReflectionOperation;
import com.newrelic.api.agent.security.utils.logging.LogLevel;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Weave(type = MatchType.ExactClass, originalName = "java.lang.reflect.Method")
public abstract class Method_Instrumentation {

public abstract String getName();

public abstract Class<?> getDeclaringClass();

public abstract Class<?>[] getParameterTypes();

public Object invoke(Object obj, Object... args) {
AbstractOperation operation = null;
if(NewRelicSecurity.isHookProcessingActive()) {
operation = preprocessSecurityHook(obj, getDeclaringClass(), getParameterTypes(), getName(), args);
}
Object returnValue = Weaver.callOriginal();
registerExitOperation(operation);
return returnValue;
}

private void registerExitOperation(AbstractOperation operation) {
if (operation == null || !NewRelicSecurity.isHookProcessingActive() ||
NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || GenericHelper.skipExistsEvent()
) {
return;
}
NewRelicSecurity.getAgent().registerExitEvent(operation);
}

private AbstractOperation preprocessSecurityHook(Object obj, Class<?> declaringClass, Class<?>[] parameterTypes, String name, Object[] args) {
try {
if(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || !GenericHelper.isLockAcquirePossible(VulnerabilityCaseType.REFLECTION)) {
return null;
}

JavaReflectionOperation operation = new JavaReflectionOperation(this.getClass().getName(), "invoke", declaringClass.getName(), name, args, obj);
List<String> methodNames = new ArrayList<>();
for (Method method : declaringClass.getDeclaredMethods()) {
if(Arrays.equals(method.getParameterTypes(), parameterTypes)) {
methodNames.add(method.getName());
}
}
operation.setDeclaredMethods(methodNames);
NewRelicSecurity.getAgent().registerOperation(operation);
return operation;
} catch (Throwable e) {
if(e instanceof NewRelicSecurityException){
NewRelicSecurity.getAgent().log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, "JAVA-REFLECTION", e.getMessage()), e, Method_Instrumentation.class.getName());
throw e;
}
NewRelicSecurity.getAgent().log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, "JAVA-REFLECTION", e.getMessage()), e, Method_Instrumentation.class.getName());
NewRelicSecurity.getAgent().reportIncident(LogLevel.SEVERE , String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, "JAVA-REFLECTION", e.getMessage()), e, Method_Instrumentation.class.getName());
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ private void readSkipScan() throws RestrictionModeException {
agentMode.getSkipScan().getIastDetectionCategory().setXpathInjectionEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_XPATH_INJECTION, false));
agentMode.getSkipScan().getIastDetectionCategory().setSsrfEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_SSRF, false));
agentMode.getSkipScan().getIastDetectionCategory().setRxssEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_RXSS, false));
agentMode.getSkipScan().getIastDetectionCategory().setUnsafeDeserializationEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_UNSAFE_DESERIALIZATION, false));
agentMode.getSkipScan().getIastDetectionCategory().setInsecureReflectionEnabled(NewRelic.getAgent().getConfig().getValue(SKIP_UNSAFE_REFLECTION, false));
if(!agentMode.getSkipScan().getIastDetectionCategory().getRxssEnabled() && !NewRelic.getAgent().getConfig().getValue(REPORT_HTTP_RESPONSE_BODY, true)) {
agentMode.getSkipScan().getIastDetectionCategory().setRxssEnabled(true);
}
Expand Down Expand Up @@ -316,7 +318,7 @@ private String applyRequiredLogLevel() {
if(value instanceof Boolean) {
logLevel = IUtilConstants.OFF;
} else {
logLevel = NewRelic.getAgent().getConfig().getValue(IUtilConstants.NR_LOG_LEVEL, IUtilConstants.INFO);
logLevel = NewRelic.getAgent().getConfig().getValue(IUtilConstants.NR_LOG_LEVEL, LogLevel.FINEST.name());
}

try {
Expand Down
Loading