Skip to content
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

IAST security controls: handling custom validation and sanitization methods #7997

Merged
merged 41 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0f9c22c
formatter and config POC
jandro996 Nov 15, 2024
a44168e
more changes
jandro996 Nov 19, 2024
3b19a16
Not working maybe need to rollback
jandro996 Nov 20, 2024
8083908
use IastSystem
jandro996 Nov 20, 2024
57134bd
First working version
jandro996 Nov 21, 2024
8e591d4
Add more stacktrace cases
jandro996 Nov 22, 2024
2c725b9
first review
jandro996 Nov 22, 2024
831fe64
Fix test and forbidden apis
jandro996 Nov 22, 2024
8602477
fix spotless
jandro996 Nov 22, 2024
325086f
fix spotless
jandro996 Nov 22, 2024
838d924
fix test
jandro996 Nov 22, 2024
f0f8496
fix test
jandro996 Nov 22, 2024
9844124
Add test and fix coverage
jandro996 Nov 22, 2024
744cdbb
remove security control enabled env var
jandro996 Nov 25, 2024
74a93b8
fix classVisitor
jandro996 Nov 25, 2024
82bb7df
change array for list
jandro996 Nov 25, 2024
7d54511
fix test
jandro996 Nov 25, 2024
8516871
Add unit test for IastSecurityControlTransformer
jandro996 Nov 25, 2024
2aa1c72
improve test
jandro996 Nov 25, 2024
21d7ed1
fix onUnexpectedException message
jandro996 Nov 25, 2024
e6225ec
fix codenarc
jandro996 Nov 26, 2024
867ab0f
change to forked tests
jandro996 Nov 26, 2024
800cc0c
fix spotless
jandro996 Nov 26, 2024
1c02218
fix coverage and tests
jandro996 Nov 26, 2024
963f7c8
fix secure marks in formatter
jandro996 Nov 26, 2024
69a3466
Add more tests
jandro996 Dec 12, 2024
dfef843
upgrade Opcodes.ASM9
jandro996 Dec 12, 2024
7c7e642
remove unnecessary change
jandro996 Dec 16, 2024
722e3f6
rename endpoint
jandro996 Dec 16, 2024
fff5baf
Add safeguard to security control class transform
jandro996 Dec 17, 2024
fab351a
Fix method adapter add logs and more test cases
jandro996 Dec 17, 2024
dee1faf
Fix jacoco
jandro996 Dec 18, 2024
7411d9b
changes suggested by manu
jandro996 Dec 19, 2024
045b376
changes suggested by manu
jandro996 Dec 19, 2024
49ad3d6
remove unnecessary check
jandro996 Dec 19, 2024
0ae0bdd
separate method visitors for adapters and input validators
jandro996 Dec 19, 2024
78e6d43
fix spotless
jandro996 Dec 19, 2024
c7290cb
Merge branch 'master' into alejandro.gonzalez/security-controls
jandro996 Dec 19, 2024
3eff5ce
Merge branch 'master' into alejandro.gonzalez/security-controls
jandro996 Dec 20, 2024
de8c198
Update dd-java-agent/agent-iast/src/main/java/com/datadog/iast/securi…
jandro996 Dec 20, 2024
70186de
Merge branch 'master' into alejandro.gonzalez/security-controls
jandro996 Dec 20, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Stream;
Expand Down Expand Up @@ -129,7 +130,7 @@ private static void maybeApplySecurityControls(@Nullable Instrumentation instrum
if (Config.get().getIastSecurityControlsConfiguration() == null || instrumentation == null) {
return;
}
List<SecurityControl> securityControls =
Map<String, List<SecurityControl>> securityControls =
SecurityControlFormatter.format(Config.get().getIastSecurityControlsConfiguration());
if (securityControls == null) {
LOGGER.warn("No security controls to apply");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.datadog.iast.securitycontrol;

import static org.objectweb.asm.ClassReader.SKIP_DEBUG;
import static org.objectweb.asm.ClassReader.SKIP_FRAMES;

import datadog.trace.api.iast.securitycontrol.SecurityControl;
import java.lang.instrument.ClassFileTransformer;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.Map;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
Expand All @@ -17,13 +19,10 @@ public class IastSecurityControlTransformer implements ClassFileTransformer {
private static final Logger LOGGER =
LoggerFactory.getLogger(IastSecurityControlTransformer.class);

private final List<SecurityControl> securityControls;
private final Set<String> classFilter;
private final Map<String, List<SecurityControl>> securityControls;

public IastSecurityControlTransformer(List<SecurityControl> securityControls) {
public IastSecurityControlTransformer(Map<String, List<SecurityControl>> securityControls) {
this.securityControls = securityControls;
this.classFilter =
securityControls.stream().map(SecurityControl::getClassName).collect(Collectors.toSet());
}

@Override
Expand All @@ -34,28 +33,22 @@ public byte[] transform(
Class<?> classBeingRedefined,
java.security.ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (!classFilter.contains(className)) {
if (!securityControls.containsKey(className)) {
jandro996 marked this conversation as resolved.
Show resolved Hide resolved
return null; // Do not transform classes that are not in the classFilter
}
List<SecurityControl> match = getSecurityControl(className);
List<SecurityControl> match = securityControls.get(className);
if (match == null || match.isEmpty()) {
return null; // Do not transform classes that do not have a security control
}
try {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new SecurityControlMethodClassVisitor(cw, match);
cr.accept(cv, 0);
cr.accept(cv, SKIP_DEBUG | SKIP_FRAMES);
return cw.toByteArray();
} catch (Throwable e) {
LOGGER.warn("Failed to transform class: {}", className, e);
return null;
}
}

private List<SecurityControl> getSecurityControl(final String className) {
return securityControls.stream()
.filter(sc -> sc.getClassName().equals(className))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,47 @@ public class SecurityControlMethodAdapter extends MethodVisitor {
private final MethodVisitor mv;
private final SecurityControl securityControl;
private final String desc;
boolean isStatic;

public SecurityControlMethodAdapter(
final MethodVisitor mv, final SecurityControl securityControl, final String desc) {
final MethodVisitor mv,
final SecurityControl securityControl,
final String desc,
boolean isStatic) {
super(Opcodes.ASM9, mv);
this.mv = mv;
this.securityControl = securityControl;
this.desc = desc;
this.isStatic = isStatic;
}

@Override
public void visitCode() {
super.visitCode();
if (securityControl.getType() == SecurityControlType.INPUT_VALIDATOR) {
processInputValidator();
}
}

@Override
public void visitInsn(int opcode) {
// Check if the opcode is a return instruction
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
if (securityControl.getType() == SecurityControlType.INPUT_VALIDATOR) {
processInputValidator();
} else { // SecurityControlType.SANITIZER
if (securityControl.getType() == SecurityControlType.SANITIZER) {
// Only process the return value if is an Object as we are not tainting primitives
if (opcode == Opcodes.ARETURN) {
processSanitizer();
} else {
Type returnType = Type.getReturnType(desc);
// no need to check primitives as we are not tainting them
LOGGER.warn(
"Sanitizers should not be used on primitive return types. Return type {}. Security control: {}",
returnType.getClassName(),
securityControl);
}
}

super.visitInsn(opcode);
}

private void processSanitizer() {
Type returnType = Type.getReturnType(desc);
if (isPrimitive(returnType)) {
// no need to check primitives as we are not tainting them
LOGGER.warn(
"Sanitizers should not be used on non-primitive return types. Return type {}. Security control: {}",
returnType.getClassName(),
securityControl);
return;
}
// Duplicate the return value on the stack
mv.visitInsn(Opcodes.DUP);
// Load the marks from securityControl onto the stack
Expand All @@ -71,7 +79,7 @@ private void processInputValidator() {
if (!isPrimitive) {
callInputValidation(parametersCount);
}
} else if (securityControl.getParametersToMark().contains(i)) {
} else if (securityControl.getParametersToMark().get(i)) {
if (isPrimitive) {
LOGGER.warn(
smola marked this conversation as resolved.
Show resolved Hide resolved
"Input validators should not be used on primitive types. Parameter {} with type {} .Security control: {}",
Expand All @@ -88,7 +96,9 @@ private void processInputValidator() {

private void callInputValidation(int i) {
// Duplicate the parameter value on the stack
mv.visitVarInsn(Opcodes.ALOAD, i);
mv.visitVarInsn(
Opcodes.ALOAD,
isStatic ? i : i + 1); // instance methods have this as first element in the stack
// Load the marks from securityControl onto the stack
mv.visitLdcInsn(securityControl.getMarks());
// Insert the call to setSecureMarks with the parameter value and marks as parameters
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.datadog.iast.securitycontrol;

import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ASM8;

import datadog.trace.api.iast.securitycontrol.SecurityControl;
Expand Down Expand Up @@ -36,7 +37,8 @@ public MethodVisitor visitMethod(
}
}
if (match != null) {
return new SecurityControlMethodAdapter(mv, match, desc);
final boolean isStatic = (access & ACC_STATIC) > 0;
return new SecurityControlMethodAdapter(mv, match, desc, isStatic);
}
return mv;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,29 @@ import datadog.trace.api.iast.propagation.PropagationModule
import datadog.trace.api.iast.securitycontrol.SecurityControl
import datadog.trace.api.iast.securitycontrol.SecurityControlFormatter
import datadog.trace.test.util.DDSpecification
import foo.bar.securitycontrol.SecurityControlStaticTestSuite
import foo.bar.securitycontrol.SecurityControlTestSuite
import net.bytebuddy.agent.ByteBuddyAgent

import java.lang.instrument.Instrumentation

class IastSecurityControlTransformerForkedTest extends DDSpecification{

//static methods
private static final String STATIC_SANITIZER = 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:sanitize'
private static final String STATIC_SANITIZE_VALIDATE_OBJECT = 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:sanitizeObject'
private static final String STATIC_SANITIZE_INPUTS= 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:sanitizeInputs'
private static final String STATIC_SANITIZE_MANY_INPUTS= 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:sanitizeManyInputs'
private static final String STATIC_SANITIZE_INT = 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:sanitizeInt'
private static final String STATIC_SANITIZE_LONG = 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:sanitizeLong'
private static final String STATIC_INPUT_VALIDATOR_VALIDATE_ALL = 'INPUT_VALIDATOR:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:validateAll'
private static final String STATIC_INPUT_VALIDATOR_VALIDATE_OVERLOADED = 'INPUT_VALIDATOR:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:validate:java.lang.Object,java.lang.String,java.lang.String:1,2'
private static final String STATIC_INPUT_VALIDATOR_VALIDATE_RETURNING_INT = 'INPUT_VALIDATOR:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:validateReturningInt'
private static final String STATIC_INPUT_VALIDATOR_VALIDATE_OBJECT = 'INPUT_VALIDATOR:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:validateObject'
private static final String STATIC_INPUT_VALIDATOR_VALIDATE_LONG = 'INPUT_VALIDATOR:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:validateLong'
private static final String STATIC_INPUT_VALIDATOR_VALIDATE_SELECTED_LONG = 'INPUT_VALIDATOR:XSS:foo.bar.securitycontrol.SecurityControlStaticTestSuite:validateSelectedLong:0'

//not static methods
private static final String SANITIZER = 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlTestSuite:sanitize'
private static final String SANITIZE_VALIDATE_OBJECT = 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlTestSuite:sanitizeObject'
private static final String SANITIZE_INPUTS= 'SANITIZER:XSS:foo.bar.securitycontrol.SecurityControlTestSuite:sanitizeInputs'
Expand All @@ -29,10 +45,12 @@ class IastSecurityControlTransformerForkedTest extends DDSpecification{


def setupSpec() {
final config = "${SANITIZER};${SANITIZE_VALIDATE_OBJECT};${SANITIZE_INPUTS};${SANITIZE_MANY_INPUTS};${SANITIZE_INT};${SANITIZE_LONG};${INPUT_VALIDATOR_VALIDATE_ALL};${INPUT_VALIDATOR_VALIDATE_OVERLOADED};${INPUT_VALIDATOR_VALIDATE_RETURNING_INT};${INPUT_VALIDATOR_VALIDATE_OBJECT};${INPUT_VALIDATOR_VALIDATE_LONG};${INPUT_VALIDATOR_VALIDATE_SELECTED_LONG}"
final staticConfig = "${STATIC_SANITIZER};${STATIC_SANITIZE_VALIDATE_OBJECT};${STATIC_SANITIZE_INPUTS};${STATIC_SANITIZE_MANY_INPUTS};${STATIC_SANITIZE_INT};${STATIC_SANITIZE_LONG};${STATIC_INPUT_VALIDATOR_VALIDATE_ALL};${STATIC_INPUT_VALIDATOR_VALIDATE_OVERLOADED};${STATIC_INPUT_VALIDATOR_VALIDATE_RETURNING_INT};${STATIC_INPUT_VALIDATOR_VALIDATE_OBJECT};${STATIC_INPUT_VALIDATOR_VALIDATE_LONG};${STATIC_INPUT_VALIDATOR_VALIDATE_SELECTED_LONG}"
final config = "${SANITIZER};${SANITIZE_VALIDATE_OBJECT};${SANITIZE_INPUTS};${SANITIZE_MANY_INPUTS};${SANITIZE_INT};${SANITIZE_LONG};${INPUT_VALIDATOR_VALIDATE_ALL};${INPUT_VALIDATOR_VALIDATE_OVERLOADED};${INPUT_VALIDATOR_VALIDATE_RETURNING_INT};${INPUT_VALIDATOR_VALIDATE_OBJECT};${INPUT_VALIDATOR_VALIDATE_LONG};${INPUT_VALIDATOR_VALIDATE_SELECTED_LONG}"
final fullConfig = "${staticConfig};${config}"
Instrumentation instrumentation = ByteBuddyAgent.install()
List<SecurityControl> securityControls =
SecurityControlFormatter.format(config)
Map<String,List<SecurityControl>> securityControls =
SecurityControlFormatter.format(fullConfig)
assert securityControls != null
instrumentation.addTransformer(new IastSecurityControlTransformer(securityControls), true)
}
Expand All @@ -45,7 +63,15 @@ class IastSecurityControlTransformerForkedTest extends DDSpecification{
final marks = (VulnerabilityMarks.XSS_MARK | VulnerabilityMarks.CUSTOM_SECURITY_CONTROL_MARK)

when:
SecurityControlTestSuite.&"$method".call(*args)
SecurityControlStaticTestSuite.&"$method".call(*args)

then:
expected * iastModule.markIfTainted( toSanitize, marks)
0 * _

when:
final suite = new SecurityControlTestSuite()
suite.&"$method".call(*args)

then:
expected * iastModule.markIfTainted( toSanitize, marks)
Expand All @@ -68,7 +94,17 @@ class IastSecurityControlTransformerForkedTest extends DDSpecification{
final marks = (VulnerabilityMarks.XSS_MARK | VulnerabilityMarks.CUSTOM_SECURITY_CONTROL_MARK)

when:
SecurityControlTestSuite.&"$method".call(*args)
SecurityControlStaticTestSuite.&"$method".call(*args)

then:
for (final validate : toValidate){
expected * iastModule.markIfTainted(validate, marks)
}
0 * _

when:
final suite = new SecurityControlTestSuite()
suite.&"$method".call(*args)

then:
for (final validate : toValidate){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package foo.bar.securitycontrol;

public class SecurityControlStaticTestSuite {

public static boolean validateAll(long input, String input2) {
return true; // dummy implementation
}

public static boolean validateAll(String input) {
return true; // dummy implementation
}

public static boolean validateAll(String input, String input2) {
return true; // dummy implementation
}

public static boolean validateAll(
String input,
String input2,
String input3,
String input4,
String input5,
String input6,
String input7,
String input8,
String input9,
String input10) {
return true; // dummy implementation
}

public static boolean validateLong(long input, String input2) {
return true; // dummy implementation
}

public static boolean validateLong(String input, long input2) {
return true; // dummy implementation
}

public static boolean validateLong(long intput1, String input2, long input3) {
return true; // dummy implementation
}

public static boolean validateSelectedLong(long intput1) {
return true; // dummy implementation
}

public static boolean validateSelectedLong(long input1, long intput2) {
return true; // dummy implementation
}

public static boolean validate(String input) {
return true; // dummy implementation
}

public static boolean validate(Object o, String input, String input2) {
return true; // dummy implementation
}

public static int validateReturningInt(String input) {
return 1; // dummy implementation
}

public static int validateObject(Object input) {
return 1; // dummy implementation
}

public static String sanitize(String input) {
return "Sanitized";
}

public static Object sanitizeObject(String input) {
return "Sanitized";
}

public static String sanitizeInputs(String input, Object input2, int input3) {
return "Sanitized";
}

public static String sanitizeManyInputs(
String input,
String input2,
String input3,
String input4,
String input5,
String input6,
String input7,
String input8,
String input9,
String input10) {
return "Sanitized";
}

public static int sanitizeInt(int input) {
return input;
}

public static long sanitizeLong(long input) {
return input;
}
}
Loading
Loading