-
Notifications
You must be signed in to change notification settings - Fork 5
Fix request context propagation for async executor tasks #305
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package dev.aikido.agent.wrappers.executor; | ||
|
|
||
| import dev.aikido.agent.wrappers.Wrapper; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| import java.util.concurrent.AbstractExecutorService; | ||
| import java.util.concurrent.Callable; | ||
|
|
||
| import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| public class AbstractExecutorServiceWrapper implements Wrapper { | ||
| @Override | ||
| public String getName() { | ||
| return SubmitAdvice.class.getName(); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getMatcher() { | ||
| return isMethod() | ||
| .and(named("submit")) | ||
| .and( | ||
| takesArguments(Runnable.class) | ||
| .or(takesArguments(Callable.class)) | ||
| .or(takesArguments(Runnable.class, Object.class)) | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getTypeMatcher() { | ||
| return isSubTypeOf(AbstractExecutorService.class); | ||
| } | ||
|
|
||
| public static class SubmitAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void before( | ||
| @Advice.Argument(value = 0, readOnly = false, typing = DYNAMIC) Object task | ||
| ) { | ||
| if (task instanceof Runnable) { | ||
| task = ExecutorContextPropagation.wrap((Runnable) task); | ||
| } else if (task instanceof Callable) { | ||
| task = ExecutorContextPropagation.wrap((Callable) task); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package dev.aikido.agent.wrappers.executor; | ||
|
|
||
| import dev.aikido.agent.wrappers.Wrapper; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| import java.lang.reflect.Method; | ||
| import java.net.URL; | ||
| import java.net.URLClassLoader; | ||
| import java.util.concurrent.Callable; | ||
|
|
||
| import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| public class DelegatedExecutorServiceWrapper implements Wrapper { | ||
| @Override | ||
| public String getName() { | ||
| return DelegatedExecutorAdvice.class.getName(); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getMatcher() { | ||
| return isMethod() | ||
| .and(named("execute").or(named("submit"))) | ||
| .and( | ||
| takesArguments(Runnable.class) | ||
| .or(takesArguments(Callable.class)) | ||
| .or(takesArguments(Runnable.class, Object.class)) | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getTypeMatcher() { | ||
| return nameStartsWith("java.util.concurrent.Executors$"); | ||
| } | ||
|
|
||
| public static class DelegatedExecutorAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void before( | ||
| @Advice.Argument(value = 0, readOnly = false, typing = DYNAMIC) Object task | ||
| ) throws Exception { | ||
| if (task == null) { | ||
| return; | ||
| } | ||
|
|
||
| // This advice is applied to JDK classes loaded by the bootstrap classloader. | ||
| // Load agent_api reflectively because bootstrap classes cannot directly reference agent classes. | ||
| String jarFilePath = System.getProperty("AIK_agent_api_jar"); | ||
| if (jarFilePath == null || jarFilePath.isBlank()) { | ||
| return; | ||
| } | ||
|
|
||
| URLClassLoader classLoader = new URLClassLoader(new URL[] { new URL(jarFilePath) }); | ||
| Class<?> contextPropagationClass = classLoader.loadClass( | ||
| "dev.aikido.agent_api.context.ContextPropagation" | ||
| ); | ||
|
|
||
| if (task instanceof Runnable) { | ||
| Method wrapRunnable = contextPropagationClass.getMethod("wrap", Runnable.class); | ||
| task = wrapRunnable.invoke(null, task); | ||
| } else if (task instanceof Callable) { | ||
| Method wrapCallable = contextPropagationClass.getMethod("wrap", Callable.class); | ||
| task = wrapCallable.invoke(null, task); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,67 @@ | ||||||||||||||
| package dev.aikido.agent.wrappers.executor; | ||||||||||||||
|
|
||||||||||||||
| import java.lang.reflect.Method; | ||||||||||||||
| import java.net.URL; | ||||||||||||||
| import java.net.URLClassLoader; | ||||||||||||||
| import java.util.concurrent.Callable; | ||||||||||||||
|
|
||||||||||||||
| public final class ExecutorContextPropagation { | ||||||||||||||
| private static Method wrapRunnableMethod; | ||||||||||||||
| private static Method wrapCallableMethod; | ||||||||||||||
| private static boolean disabled; | ||||||||||||||
|
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Static fields wrapRunnableMethod/wrapCallableMethod/disabled are unsafely accessed from multiple threads; make accesses thread-safe (e.g., use synchronization or volatile) to avoid visibility races. Show fix
Suggested change
Details✨ AI Reasoning Reply |
||||||||||||||
|
|
||||||||||||||
| private ExecutorContextPropagation() {} | ||||||||||||||
|
|
||||||||||||||
| public static Runnable wrap(Runnable task) { | ||||||||||||||
| if (task == null || disabled) { | ||||||||||||||
| return task; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| init(); | ||||||||||||||
| if (wrapRunnableMethod == null) { | ||||||||||||||
| return task; | ||||||||||||||
| } | ||||||||||||||
| return (Runnable) wrapRunnableMethod.invoke(null, task); | ||||||||||||||
| } catch (Throwable ignored) { | ||||||||||||||
| disabled = true; | ||||||||||||||
| return task; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @SuppressWarnings("unchecked") | ||||||||||||||
| public static <T> Callable<T> wrap(Callable<T> task) { | ||||||||||||||
| if (task == null || disabled) { | ||||||||||||||
| return task; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| init(); | ||||||||||||||
| if (wrapCallableMethod == null) { | ||||||||||||||
| return task; | ||||||||||||||
| } | ||||||||||||||
| return (Callable<T>) wrapCallableMethod.invoke(null, task); | ||||||||||||||
| } catch (Throwable ignored) { | ||||||||||||||
| disabled = true; | ||||||||||||||
| return task; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private static synchronized void init() throws Exception { | ||||||||||||||
| if (disabled || wrapRunnableMethod != null) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| String jarFilePath = System.getProperty("AIK_agent_api_jar"); | ||||||||||||||
| if (jarFilePath == null || jarFilePath.isBlank()) { | ||||||||||||||
| disabled = true; | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| URLClassLoader classLoader = new URLClassLoader(new URL[] { new URL(jarFilePath) }); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reflective loading of ContextPropagation via URLClassLoader from System.getProperty("AIK_agent_api_jar") and resolving wrap methods obscures where behavior is implemented; this dynamic loading can hide malicious code. Prefer safe, explicit classloading or validate the artifact. Details✨ AI Reasoning 🔧 How do I fix it? Reply |
||||||||||||||
| Class<?> clazz = classLoader.loadClass("dev.aikido.agent_api.context.ContextPropagation"); | ||||||||||||||
|
|
||||||||||||||
| wrapRunnableMethod = clazz.getMethod("wrap", Runnable.class); | ||||||||||||||
| wrapCallableMethod = clazz.getMethod("wrap", Callable.class); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package dev.aikido.agent.wrappers.executor; | ||
|
|
||
| import dev.aikido.agent.wrappers.Wrapper; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| import java.util.concurrent.Callable; | ||
| import java.util.concurrent.ForkJoinPool; | ||
|
|
||
| import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| public class ForkJoinPoolWrapper implements Wrapper { | ||
| @Override | ||
| public String getName() { | ||
| return ForkJoinAdvice.class.getName(); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getMatcher() { | ||
| return isMethod() | ||
| .and(named("execute").or(named("submit"))) | ||
| .and( | ||
| takesArguments(Runnable.class) | ||
| .or(takesArguments(Callable.class)) | ||
| .or(takesArguments(Runnable.class, Object.class)) | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getTypeMatcher() { | ||
| return isSubTypeOf(ForkJoinPool.class); | ||
| } | ||
|
|
||
| public static class ForkJoinAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void before( | ||
| @Advice.Argument(value = 0, readOnly = false, typing = DYNAMIC) Object task | ||
| ) { | ||
| if (task instanceof Runnable) { | ||
| task = ExecutorContextPropagation.wrap((Runnable) task); | ||
| } else if (task instanceof Callable) { | ||
| task = ExecutorContextPropagation.wrap((Callable) task); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package dev.aikido.agent.wrappers.executor; | ||
|
|
||
| import dev.aikido.agent.wrappers.Wrapper; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| import java.lang.reflect.Method; | ||
| import java.net.URL; | ||
| import java.net.URLClassLoader; | ||
| import java.util.concurrent.Callable; | ||
| import java.util.concurrent.ScheduledThreadPoolExecutor; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| public class ScheduledThreadPoolExecutorWrapper implements Wrapper { | ||
| @Override | ||
| public String getName() { | ||
| return ScheduleAdvice.class.getName(); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getMatcher() { | ||
| return isMethod() | ||
| .and(named("schedule")) | ||
| .and( | ||
| takesArguments(Runnable.class, long.class, TimeUnit.class) | ||
| .or(takesArguments(Callable.class, long.class, TimeUnit.class)) | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getTypeMatcher() { | ||
| return isSubTypeOf(ScheduledThreadPoolExecutor.class); | ||
| } | ||
|
|
||
| public static class ScheduleAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void before( | ||
| @Advice.Argument(value = 0, readOnly = false, typing = DYNAMIC) Object task | ||
| ) throws Exception { | ||
| if (task == null) { | ||
| return; | ||
| } | ||
|
|
||
| // This advice is applied to JDK classes loaded by the bootstrap classloader. | ||
| // Load agent_api reflectively because bootstrap classes cannot directly reference agent classes. | ||
| String jarFilePath = System.getProperty("AIK_agent_api_jar"); | ||
| if (jarFilePath == null || jarFilePath.isBlank()) { | ||
| return; | ||
| } | ||
|
|
||
| URLClassLoader classLoader = new URLClassLoader(new URL[] { new URL(jarFilePath) }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dynamic loading of a JAR from System.getProperty("AIK_agent_api_jar") with URLClassLoader and reflective Method.invoke hides execution targets; this pattern can conceal malicious payloads. Validate and document the loaded artifact and minimize reflective use. Details✨ AI Reasoning 🔧 How do I fix it? Reply |
||
| Class<?> contextPropagationClass = classLoader.loadClass( | ||
| "dev.aikido.agent_api.context.ContextPropagation" | ||
| ); | ||
|
|
||
| if (task instanceof Runnable) { | ||
| Method wrapRunnable = contextPropagationClass.getMethod("wrap", Runnable.class); | ||
| task = wrapRunnable.invoke(null, task); | ||
| } else if (task instanceof Callable) { | ||
| Method wrapCallable = contextPropagationClass.getMethod("wrap", Callable.class); | ||
| task = wrapCallable.invoke(null, task); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package dev.aikido.agent.wrappers.executor; | ||
|
|
||
| import dev.aikido.agent.wrappers.Wrapper; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| import java.util.concurrent.ThreadPoolExecutor; | ||
|
|
||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| public class ThreadPoolExecutorWrapper implements Wrapper { | ||
| @Override | ||
| public String getName() { | ||
| return ExecuteAdvice.class.getName(); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getMatcher() { | ||
| return isMethod() | ||
| .and(named("execute")) | ||
| .and(takesArguments(Runnable.class)); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher getTypeMatcher() { | ||
| return isSubTypeOf(ThreadPoolExecutor.class); | ||
| } | ||
|
|
||
| public static class ExecuteAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void before( | ||
| @Advice.Argument(value = 0, readOnly = false) Runnable task | ||
| ) { | ||
| task = ExecutorContextPropagation.wrap(task); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dynamic loading of a JAR from System.getProperty("AIK_agent_api_jar") and reflective invocation (URLClassLoader + Class.getMethod + Method.invoke) hides call targets and control flow; this pattern can conceal malicious behavior. Avoid or clearly document and validate the loaded artifact.
Details
✨ AI Reasoning
The change adds code that loads a JAR path from a system property and then constructs a URLClassLoader and uses reflection to load and invoke methods. Loading code dynamically from a path derived at runtime and invoking methods via reflection hides static call targets and control flow from readers and static analyzers, which can be used to conceal malicious behavior. This behavior was introduced in this PR where DelegatedExecutorServiceWrapper reflectively loads ContextPropagation and invokes its wrap methods using Method.invoke. The code paths accept an external system property (AIK_agent_api_jar) and dynamically load code from it, which is a typical pattern used to obscure execution and hide functionality.
🔧 How do I fix it?
Ensure code is transparent and not intentionally obfuscated. Avoid hiding functionality from code review. Focus on intent and deception, not specific patterns.
Reply
@AikidoSec feedback: [FEEDBACK]to get better review comments in the future.Reply
@AikidoSec ignore: [REASON]to ignore this issue.More info