diff --git a/gradle.properties b/gradle.properties index 95e22a3e4..4bd015446 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # The agent version. agentVersion=1.6.0 -jsonVersion=1.2.10 +jsonVersion=1.2.11 # Updated exposed NR APM API version. nrAPIVersion=8.12.0 diff --git a/instrumentation-security/deserialisation/build.gradle b/instrumentation-security/deserialisation/build.gradle new file mode 100644 index 000000000..40fb3a3e9 --- /dev/null +++ b/instrumentation-security/deserialisation/build.gradle @@ -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 +} diff --git a/instrumentation-security/deserialisation/src/main/java/com/newrelic/csec/validator/scanner/DeltaClass.java b/instrumentation-security/deserialisation/src/main/java/com/newrelic/csec/validator/scanner/DeltaClass.java new file mode 100644 index 000000000..b578fde1e --- /dev/null +++ b/instrumentation-security/deserialisation/src/main/java/com/newrelic/csec/validator/scanner/DeltaClass.java @@ -0,0 +1,22 @@ +package com.newrelic.csec.validator.scanner; + +import java.io.Serializable; + +public class DeltaClass implements Serializable { + + private static final long serialVersionUID = 5220788560470736671L; + + public String field0; + + public DeltaClass(String field0) { + this.field0 = field0; + } + + public String getField0() { + return field0; + } + + public void setField0(String field0) { + this.field0 = field0; + } +} diff --git a/instrumentation-security/deserialisation/src/main/java/java/io/ObjectInputStream_Instrumentation.java b/instrumentation-security/deserialisation/src/main/java/java/io/ObjectInputStream_Instrumentation.java new file mode 100644 index 000000000..e4037bd38 --- /dev/null +++ b/instrumentation-security/deserialisation/src/main/java/java/io/ObjectInputStream_Instrumentation.java @@ -0,0 +1,66 @@ +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.AbstractOperation; +import com.newrelic.api.agent.security.schema.DeserializationInfo; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.DeserialisationOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + + +@Weave(type = MatchType.ExactClass, originalName = "java.io.ObjectInputStream") +public abstract class ObjectInputStream_Instrumentation { + + private void readSerialData(Object obj, ObjectStreamClass desc) + throws IOException { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + DeserializationInfo dInfo = null; + if(isLockAcquired) { + dInfo = preProcessSecurityHook(obj); + } + try { + Weaver.callOriginal(); + operation = postProcessSecurityHook(dInfo); + } finally { + if(isLockAcquired) { + finalProcessSecurityHook(dInfo); + GenericHelper.releaseLock(SecurityHelper.NR_SEC_CUSTOM_ATTRIB_NAME); + } + } + //TODO add register exit operation if required + } + + private DeserializationInfo preProcessSecurityHook(Object obj) { + DeserializationInfo dInfo = new DeserializationInfo(obj.getClass().getName(), obj); + NewRelicSecurity.getAgent().getSecurityMetaData().addToDeserializationRoot(dInfo); + return dInfo; + } + + private DeserialisationOperation postProcessSecurityHook(DeserializationInfo dInfo) { + if (dInfo != null && NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot() == dInfo) { + DeserialisationOperation operation = new DeserialisationOperation( + this.getClass().getName(), + SecurityHelper.METHOD_NAME_READ_OBJECT + ); + NewRelicSecurity.getAgent().registerOperation(operation); + return operation; + } + return null; + } + + private void finalProcessSecurityHook(DeserializationInfo dInfo) { + if (dInfo != null && + NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot() != null && + NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot() == dInfo) { + NewRelicSecurity.getAgent().getSecurityMetaData().resetDeserializationRoot(); + } + } + + private boolean acquireLockIfPossible() { + return GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.UNSAFE_DESERIALIZATION, SecurityHelper.NR_SEC_CUSTOM_ATTRIB_NAME); + } +} \ No newline at end of file diff --git a/instrumentation-security/deserialisation/src/main/java/java/io/SecurityHelper.java b/instrumentation-security/deserialisation/src/main/java/java/io/SecurityHelper.java new file mode 100644 index 000000000..daed02acb --- /dev/null +++ b/instrumentation-security/deserialisation/src/main/java/java/io/SecurityHelper.java @@ -0,0 +1,9 @@ +package java.io; + +public class SecurityHelper { + public static final String METHOD_NAME_READ_OBJECT = "readObject"; + public static final String NULL_STRING = "null"; + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "UNSAFE-DESERIALISATION-LOCK-JAVA-IO-"; + +} \ No newline at end of file diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java index 1064450cb..3e66de5fd 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java @@ -231,6 +231,8 @@ public Object call() throws Exception { SolrDbOperation solrDbOperation = (SolrDbOperation) operation; eventBean = prepareSolrDbRequestEvent(eventBean, solrDbOperation); break; + case UNSAFE_DESERIALIZATION: + prepareDeserializationEvent(eventBean, (DeserialisationOperation) operation); default: } @@ -653,6 +655,20 @@ private static JavaAgentEventBean prepareSSRFEvent(JavaAgentEventBean eventBean, return eventBean; } + private static JavaAgentEventBean prepareDeserializationEvent(JavaAgentEventBean eventBean, + DeserialisationOperation deserialisationOperation) { + DeserializationInfo rootDeserializationInfo = deserialisationOperation.getRootDeserializationInfo(); + JSONArray params = new JSONArray(); + if(rootDeserializationInfo != null) { + JSONObject deserializationInfo = new JSONObject(); + deserializationInfo.put("type", rootDeserializationInfo.getType()); + deserializationInfo.put("value", GsonUtil.toJson(rootDeserializationInfo.getInstance())); + params.add(deserializationInfo); + } + eventBean.setParameters(params); + return eventBean; + } + private boolean allowedExtensionFileIO(JSONArray params, String sourceString, String url) { if (JAVA_IO_FILE_INPUTSTREAM_OPEN.equals(sourceString)) { for (int i = 0; i < params.size(); i++) { diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java index 6bbaca47c..6cc61f8f6 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java @@ -104,6 +104,9 @@ private static boolean isLockAcquirePossible(VulnerabilityCaseType caseType) { case HASH: enabled = NewRelicSecurity.getAgent().getIastDetectionCategory().getInsecureSettingsEnabled(); break; + case UNSAFE_DESERIALIZATION: + enabled = NewRelicSecurity.getAgent().getIastDetectionCategory().getUnsafeDeserializationEnabled(); + break; default: break; } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AbstractOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AbstractOperation.java index c7e1e3cbb..abf87b4ba 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AbstractOperation.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AbstractOperation.java @@ -1,5 +1,7 @@ package com.newrelic.api.agent.security.schema; +import com.newrelic.api.agent.security.NewRelicSecurity; + public abstract class AbstractOperation { public static final String EMPTY = ""; @@ -25,6 +27,8 @@ public abstract class AbstractOperation { private boolean isLowSeverityHook; + private DeserializationInfo deserializationInfo; + public AbstractOperation() { this.className = EMPTY; this.sourceMethod = EMPTY; @@ -36,9 +40,16 @@ public AbstractOperation() { } public AbstractOperation(String className, String methodName){ - this.className = className; - this.methodName = methodName; - this.blockingEndTime = 0L; + this.className = className; + this.methodName = methodName; + this.blockingEndTime = 0L; + if (NewRelicSecurity.getAgent() != null && + NewRelicSecurity.getAgent().getSecurityMetaData() != null && + NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot() != null) { + this.deserializationInfo = NewRelicSecurity.getAgent().getSecurityMetaData() + .peekDeserializationRoot(); +// this.deserializationInfo.computeObjectMap(); + } } public String getClassName() { @@ -135,4 +146,12 @@ public boolean isLowSeverityHook() { public void setLowSeverityHook(boolean lowSeverityHook) { this.isLowSeverityHook = lowSeverityHook; } + + public DeserializationInfo getDeserializationInfo() { + return deserializationInfo; + } + + public void setDeserializationInfo(DeserializationInfo deserializationInfo) { + this.deserializationInfo = deserializationInfo; + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java index dd35b63b0..b05381bcd 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java @@ -229,4 +229,5 @@ public SkipScanParameters getSkipScanParameters() { public void setSkipScanParameters(SkipScanParameters skipScanParameters) { this.skipScanParameters = skipScanParameters; } + } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/DeserializationInfo.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/DeserializationInfo.java new file mode 100644 index 000000000..b23acc615 --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/DeserializationInfo.java @@ -0,0 +1,52 @@ +package com.newrelic.api.agent.security.schema; + +import java.util.ArrayList; +import java.util.List; + + +public class DeserializationInfo { + + private String type; + private List unlinkedChildren = new ArrayList<>(); + private Object instance; + + public DeserializationInfo(String type, Object instance) { + this.type = type; + this.instance = instance; + } + + public DeserializationInfo(DeserializationInfo instance) { + if (instance == null) { + return; + } + this.type = instance.type; +// for(DeserializationInfo value: instance.unlinkedChildren){ +// value.computeObjectMap(); +// this.unlinkedChildren.add(new DeserializationInfo(value)); +// } + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Object getInstance() { + return instance; + } + + public void setInstance(Object instance) { + this.instance = instance; + } + + public List getUnlinkedChildren() { + return unlinkedChildren; + } + + public void setUnlinkedChildren(List unlinkedChildren) { + this.unlinkedChildren = unlinkedChildren; + } +} \ No newline at end of file diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/SecurityMetaData.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/SecurityMetaData.java index 4c46d9b95..2bf643609 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/SecurityMetaData.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/SecurityMetaData.java @@ -125,4 +125,20 @@ public void clearCustomAttr(){ customData.clear(); } + public void addToDeserializationRoot(DeserializationInfo dinfo) { + if (getCustomAttribute("deserializationRoot", DeserializationInfo.class) == null){ + addCustomAttribute("deserializationRoot", dinfo); + } else { + DeserializationInfo root = getCustomAttribute("deserializationRoot", DeserializationInfo.class); + root.getUnlinkedChildren().add(dinfo); + } + } + + public void resetDeserializationRoot() { + removeCustomAttribute("deserializationRoot"); + } + + public DeserializationInfo peekDeserializationRoot() { + return getCustomAttribute("deserializationRoot", DeserializationInfo.class); + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java index a3d8a31d2..d0ed563c3 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java @@ -71,7 +71,9 @@ public enum VulnerabilityCaseType { CACHING_DATA_STORE("CACHING_DATA_STORE"), - SOLR_DB_REQUEST("SOLR_DB_REQUEST"); + SOLR_DB_REQUEST("SOLR_DB_REQUEST"), + /** Unsafe Deserialization */ + UNSAFE_DESERIALIZATION("UNSAFE_DESERIALIZATION"); /** case type. */ diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/DeserialisationOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/DeserialisationOperation.java new file mode 100644 index 000000000..901c48475 --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/DeserialisationOperation.java @@ -0,0 +1,60 @@ +package com.newrelic.api.agent.security.schema.operation; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.DeserializationInfo; +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; + +import java.util.Map; + +public class DeserialisationOperation extends AbstractOperation { + + private String entityName; + private Map params; + private DeserializationInfo rootDeserializationInfo; + + + public DeserialisationOperation(String className, String methodName) { + super(className, methodName); + if (NewRelicSecurity.getAgent().getSecurityMetaData()!= null && + NewRelicSecurity.getAgent().getSecurityMetaData().peekDeserializationRoot()!=null) { + this.entityName = NewRelicSecurity.getAgent().getSecurityMetaData() + .peekDeserializationRoot().getType(); +// this.params = NewRelicSecurity.getAgent().getSecurityMetaData() +// .peekDeserializationRoot().computeObjectMap(); + this.rootDeserializationInfo = NewRelicSecurity.getAgent().getSecurityMetaData() + .peekDeserializationRoot(); + } + this.setCaseType(VulnerabilityCaseType.UNSAFE_DESERIALIZATION); + } + + @Override + public boolean isEmpty() { + return this.rootDeserializationInfo==null || StringUtils.isEmpty(this.entityName); + } + + public String getEntityName() { + return entityName; + } + + public void setEntityName(String entityName) { + this.entityName = entityName; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + + public DeserializationInfo getRootDeserializationInfo() { + return rootDeserializationInfo; + } + + public void setRootDeserializationInfo(DeserializationInfo rootDeserializationInfo) { + this.rootDeserializationInfo = rootDeserializationInfo; + } +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/policy/IastDetectionCategory.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/policy/IastDetectionCategory.java index 25d0439d4..a8e23c963 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/policy/IastDetectionCategory.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/policy/IastDetectionCategory.java @@ -16,6 +16,7 @@ public class IastDetectionCategory { Boolean javascriptInjectionEnabled = false; Boolean xpathInjectionEnabled = false; Boolean ssrfEnabled = false; + Boolean unsafeDeserializationEnabled = false; @JsonIgnore private String disabledCategoriesCSV; @@ -157,6 +158,10 @@ public void generateDisabledCategoriesCSV() { disabledCategoriesCSVBuilder.append(VulnerabilityCaseType.HTTP_REQUEST); disabledCategoriesCSVBuilder.append(STR_COMMA); } + if (unsafeDeserializationEnabled) { + disabledCategoriesCSVBuilder.append(VulnerabilityCaseType.UNSAFE_DESERIALIZATION); + disabledCategoriesCSVBuilder.append(STR_COMMA); + } if (disabledCategoriesCSVBuilder.length() > 0) { disabledCategoriesCSVBuilder.deleteCharAt(disabledCategoriesCSVBuilder.length() - 1); } @@ -166,4 +171,12 @@ public void generateDisabledCategoriesCSV() { public String getDisabledCategoriesCSV() { return disabledCategoriesCSV; } + + public Boolean getUnsafeDeserializationEnabled() { + return unsafeDeserializationEnabled; + } + + public void setUnsafeDeserializationEnabled(Boolean unsafeDeserializationEnabled) { + this.unsafeDeserializationEnabled = unsafeDeserializationEnabled; + } } diff --git a/settings.gradle b/settings.gradle index afec08f66..7f9d0ddd5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -41,6 +41,7 @@ include 'instrumentation:java-lang' include 'instrumentation:java-io-stream' include 'instrumentation:java-io-inputstream-jdk8' include 'instrumentation:java-io-inputstream-jdk9' +include 'instrumentation:deserialisation' include 'instrumentation:file-operation' include 'instrumentation:servlet-2.4' include 'instrumentation:servlet-5.0'