Skip to content

Add Micronaut support (blocking controllers) - #364

Open
Mishenevd wants to merge 2 commits into
mainfrom
feat/micronaut-support
Open

Mishenevd wants to merge 2 commits into
mainfrom
feat/micronaut-support

Conversation

@Mishenevd

Copy link
Copy Markdown
Contributor

Instrument Micronaut @controller methods to build the request context on the controller thread, so protection works on both event-loop and @ExecuteOn controllers without context propagation. Adds MicronautContextObject and MicronautAnnotationCollector (@Body/@QueryValue/@PathVariable/@Part/@RequestBean), inbound IP/UA/rate-limit blocking and response-status reporting.

Includes unit tests, a MicronautHyperSQL sample app and an end-to-end test wired into the e2e matrix (JDK 25). Reactive controllers are not yet supported.

Comment thread agent/build.gradle

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CVE-2026-44242 in io.micronaut:micronaut-inject - low severity
Micronaut Framework is a JVM-based full stack Java framework designed for building modular, easily testable JVM applications. Prior to 4.10.22, the bundleCache is keyed by (Locale, baseName) where the locale originates from the HTTP Accept-Language header. In applications that explicitly register a ResourceBundleMessageSource bean and serve HTML error responses, an unauthenticated attacker can exhaust heap memory by sending requests with large numbers of unique Accept-Language values, each causing a new entry in the unbounded bundleCache. This vulnerability is fixed in 4.10.22.

Details

Remediation Aikido suggests bumping this package to version 3.10.6 to resolve this issue

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Comment on lines +47 to +51
context.setBody(value); // full body, overrides everything
return;
} else if (name.equals(QUERY_VALUE) || name.equals(PART) || name.equals(REQUEST_BEAN)) {
// The whole value is stored; StringExtractor recurses into bean fields.
context.setBodyElement(parameter.getName(), value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium - Inherited Micronaut body fields are omitted from sink scanning

A Micronaut controller accepts a @Body or @RequestBean DTO whose user-controlled field is declared in a superclass, then forwards that field to a JDBC, command, or file sink. The new collector stores the DTO for scanning, but StringExtractor only reflects the concrete class's declared fields, so the inherited value is absent from StringsFromContext and the sink detector neither blocks nor reports the attack.

Show fix

Traverse the DTO superclass hierarchy when extracting structure fields, while preserving the existing transient/static/accessible-field safeguards, and add a Micronaut test with an inherited serialized field reaching a sink.

More info - Reply on this comment to give feedback or ignore the issue.

Comment thread docs/micronaut.md
@Mishenevd
Mishenevd force-pushed the feat/micronaut-support branch from 9ea80e9 to e1184f8 Compare September 28, 2026 23:24
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void after(
@Advice.Origin Executable method,
@Advice.Return(typing = DYNAMIC, readOnly = true) Object returned,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High - Void Micronaut filters are left uninstrumented, bypassing user blocking and rate limits

A Micronaut application uses the documented void @RequestFilter methods for SetUser and ShouldBlockRequest (Trigger). The wrapper matches those methods but binds @Advice.Return ... Object without optional=true, so Byte Buddy rejects the advice for void methods and leaves the original filter bytecode unchanged (Mechanism). The filters therefore run without a Zen context: SetUser cannot attach the user and ShouldBlockRequest returns no block, allowing blocked users and user/IP rate-limit decisions to continue (Consequence).

Suggested change
@Advice.Return(typing = DYNAMIC, readOnly = true) Object returned,
@Advice.Return(optional = true, typing = DYNAMIC, readOnly = true) Object returned,

More info - Reply on this comment to give feedback or ignore the issue.

Instrument Micronaut controller methods and @RequestFilter methods:
build the request context, read user input from @Body/@QueryValue/
@PathVariable/@Part/@RequestBean, and enforce IP, bot and allowlist
blocking. The context is set on the controller thread, so protection
also covers controllers offloaded with @ExecuteOn to a virtual thread;
it is carried across that hop in an identity-keyed weak store keyed by
the request, rather than on the request's own attribute map where the
application could read or serialize it. Rate limiting and user blocking
are handled by a user @ServerFilter (see docs/micronaut.md).
Add HyperSQL sample apps for Micronaut 5.1 (JDK 25) and 4.7 (JDK 17-24)
with their e2e suites, and wire both into the end2end workflow - pinning
the 5.x app to Java 25 and excluding the 4.x app from Java 25.
@Mishenevd
Mishenevd force-pushed the feat/micronaut-support branch from e1184f8 to ada6970 Compare September 28, 2026 23:51
Comment on lines +44 to +57
public ElementMatcher<? super MethodDescription> getMatcher() {
return isAnnotatedWith(
nameContainsIgnoreCase("io.micronaut.http.annotation")
.and(nameContainsIgnoreCase("Get")
.or(nameContainsIgnoreCase("Post"))
.or(nameContainsIgnoreCase("Put"))
.or(nameContainsIgnoreCase("Delete"))
.or(nameContainsIgnoreCase("Patch"))
.or(nameContainsIgnoreCase("RequestFilter"))));
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return hasSuperType(declaresMethod(getMatcher()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium - Micronaut inbound blocking is absent without an application request filter

An application installs only the documented Java agent and has no @ServerFilter/@RequestFilter of its own; a request for an unmatched route, unsupported method, or a route rejected during argument binding therefore never invokes a controller. The agent instruments only already-existing route and request-filter methods and registers no Micronaut-wide request-entry hook, so WebRequestCollector.report is never called for those requests and configured IP or bot blocks are bypassed.

Show fix

Register an agent-owned Micronaut @ServerFilter(MATCH_ALL_PATTERN) (or instrument the framework's server request entry point) so every request establishes context and enforces inbound IP/UA/allowlist decisions before routing and binding, without requiring the application to add a filter.

More info - Reply on this comment to give feedback or ignore the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant