-
Notifications
You must be signed in to change notification settings - Fork 337
docs(appsec): add AppSec developer reference docs and module AGENTS.md files #11454
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
Draft
jandro996
wants to merge
5
commits into
master
Choose a base branch
from
alejandro.gonzalez/docs-appsec
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5886d77
docs(appsec): add AppSec developer reference docs and module AGENTS.m…
jandro996 367690b
docs(appsec): remove content based on unmerged PRs
jandro996 02b9fcb
Address review comments on docs PR #11454
jandro996 44bea27
Add DATA_DEPENDENCIES coherence test to GatewayBridgeIGRegistrationSp…
jandro996 0bef12a
Move Jetty blocking rule from AGENTS.md to code comment
jandro996 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # AppSec Module | ||
|
|
||
| Extended reference: [docs/appsec/](../../docs/appsec/) | ||
|
|
||
| ## Adding a new WAF address: 4-file checklist | ||
|
|
||
| Every new WAF address requires changes in exactly 4 files. Missing any one causes a silent no-op: | ||
|
|
||
| 1. `Events.java` -- declare the event type with a unique sequential ID | ||
| 2. `InstrumentationGateway.java` -- register the callback type in `getCallback()` | ||
| 3. `KnownAddresses.java` -- declare the `Address<T>` constant and add to `fromString()` switch | ||
| 4. `GatewayBridge.java` -- subscriber field, `init()` registration, `reset()` null, handler method, `DATA_DEPENDENCIES` entry | ||
|
|
||
| `GatewayBridgeIGRegistrationSpecification` verifies that every `DATA_DEPENDENCIES` entry registers | ||
| its events in `init()`. If you add a `DATA_DEPENDENCIES` entry but forget the `registerCallback()` | ||
| call, that test fails with an explanation. | ||
|
|
||
| `KnownAddressesSpecificationForkedTest` verifies that every address is in `fromString()` and that | ||
| the total count is up to date. | ||
|
|
||
| See [docs/appsec/ig-events.md](../../docs/appsec/ig-events.md) for the full handler pattern including the retry-on-expiry loop. | ||
|
|
||
| ## `KnownAddressesSpecificationForkedTest` instance count | ||
|
|
||
| Every new `Address<?>` declared in `KnownAddresses.java` must be accompanied by incrementing the | ||
| expected count in `KnownAddressesSpecificationForkedTest`: | ||
|
|
||
| ```groovy | ||
| Address.instanceCount() == 47 // increment by 1 per new Address | ||
| ``` | ||
|
|
||
| The test fails if this is not updated. | ||
|
|
||
| ## WAF API quick reference | ||
|
|
||
| - `server.response.status` must be `Integer`, not `String` (changed in libddwaf v1.28.0) | ||
| - gRPC messages must use `runEphemeral()`, not `run()` -- persistent data caches only the first message | ||
| - Processor outputs (`_dd.appsec.s.req.body`, fingerprints) come from `ResultWithData.attributes` -- never pass them as input addresses | ||
| - `server.request.headers.no_cookies` must not include cookies -- cookies go in `server.request.cookies` | ||
|
|
||
| Full type mapping and limits: [docs/appsec/waf-api.md](../../docs/appsec/waf-api.md) | ||
|
|
||
| ## Callback guard ordering in advice | ||
|
|
||
| Fetch ALL callbacks before any early-return guard. A callback fetched after a conditional return | ||
| is silently skipped when only that callback is registered: | ||
|
|
||
| ```java | ||
| // Correct | ||
| BiFunction<...> bodyCallback = cbp.getCallback(EVENTS.requestBodyProcessed()); | ||
| BiFunction<...> filenamesCallback = cbp.getCallback(EVENTS.requestFilesFilenames()); | ||
| BiFunction<...> contentCallback = cbp.getCallback(EVENTS.requestFilesContent()); | ||
| if (bodyCallback == null && filenamesCallback == null && contentCallback == null) return; | ||
| ``` | ||
|
|
||
| ## Blocking model per framework | ||
|
|
||
| | Framework | Call `effectivelyBlocked()`? | Notes | | ||
| |---|---|---| | ||
| | Netty | NEVER | `BlockingResponseHandler` does it internally | | ||
| | Tomcat / Jersey / Jetty | YES, after `t = new BlockingException(...)` | Inside `if (brf != null)` | | ||
| | Undertow | YES, but `tryCommitBlockingResponse` is not idempotent | Guard `t == null` before each call | | ||
| | Vert.x | NO | Handler-based model | | ||
|
|
||
| Full details: [docs/appsec/blocking-patterns.md](../../docs/appsec/blocking-patterns.md) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Akka HTTP AppSec Instrumentation | ||
|
|
||
| Extended reference: [docs/appsec/ig-events.md](../../../../docs/appsec/ig-events.md), | ||
| [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md) | ||
|
|
||
| ## Two multipart routes must both be instrumented | ||
|
|
||
| Akka HTTP has two independent entry points for multipart form data. Any new WAF address that | ||
| captures multipart body data must instrument both: | ||
|
|
||
| | Route | Entry point | Notes | | ||
| |---|---|---| | ||
| | Route 1 | `handleMultipartStrictFormData(Multipart$FormData$Strict)` | Has `reqCtx` as local variable; iterates via `getStrictParts()` | | ||
| | Route 2 | `handleStrictFormData(StrictForm)` | No `reqCtx` in scope; must obtain via `activeSpan()` | | ||
|
|
||
| If only Route 1 is instrumented, multipart requests processed via `formFieldMultiMap` (Route 2) | ||
| silently miss the WAF event. | ||
|
|
||
| ## Do not extract filenames callback dispatch into `UnmarshallerHelpers` | ||
|
|
||
| Keep the `requestFilesFilenames` callback dispatch inline in each advice class. The two routes | ||
| obtain `reqCtx` differently and handle the `pendingBlock` state separately. When the dispatch was | ||
| previously extracted into a shared helper, the filenames callback was silently skipped on Route 2 | ||
| when the body had already triggered blocking, because the helper did not propagate the pending | ||
| `BlockingException` correctly across the two call sites. | ||
|
|
||
| ## No `effectivelyBlocked()` in Akka advice | ||
|
|
||
| Akka HTTP uses the Netty-style blocking model. Do not call `effectivelyBlocked()` in advice. | ||
| See [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md#netty-never-effectivelyblocked). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Jetty AppSec Instrumentation | ||
|
|
||
| Extended reference: [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Netty 4.1 AppSec Instrumentation | ||
|
|
||
| Extended reference: [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md) | ||
|
|
||
| ## NEVER call `effectivelyBlocked()` in Netty advice | ||
|
|
||
| `effectivelyBlocked()` must not appear in any Netty advice class or helper. `BlockingResponseHandler` calls it internally when the blocking response is committed. Calling it in advice causes a double-invocation and closes the span prematurely. | ||
|
|
||
| This applies even to the urlencoded body-processed path in `HttpPostRequestDecoderInstrumentation` -- the span is already closed synchronously by `tryCommitBlockingResponse()` before advice can reach the mark. | ||
|
|
||
| Correct pattern: | ||
| ```java | ||
| // Netty advice — blocking path | ||
| if (brf != null) { | ||
| brf.tryCommitBlockingResponse(ctx.getTraceSegment(), rba); | ||
| thr = new BlockingException("..."); | ||
| // do NOT call ctx.getTraceSegment().effectivelyBlocked() | ||
| } | ||
| ``` | ||
|
|
||
| ## `thr` creation must be inside `if (brf != null)` | ||
|
|
||
| If `thr = new BlockingException(...)` is placed outside the `brf != null` guard, it is created even when no blocking action exists. The exception is then thrown unconditionally in `@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)`, producing spurious 500 responses. | ||
|
|
||
| ## FileUpload content: two-branch read | ||
|
|
||
| `HttpData` stores uploads either in memory or on disk depending on size. Always branch on `isInMemory()`: | ||
|
|
||
| ```java | ||
| if (fileUpload.isInMemory()) { | ||
| ByteBuf buf = fileUpload.getByteBuf(); | ||
| // read from buf | ||
| } else { | ||
| File f = fileUpload.getFile(); | ||
| try (InputStream is = new FileInputStream(f)) { | ||
| // read from stream | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Never call `getFile()` on an in-memory upload -- the file does not exist on disk and the call throws. | ||
|
|
||
| ## `@RequiresRequestContext` + `Config.get()` causes muzzle failure | ||
|
|
||
| Do not declare `static final` fields initialized from `Config.get()` in any `@RequiresRequestContext`-annotated advice inner class. Muzzle validates those classes against the instrumented library's classpath (Netty), where `Config` is absent, causing `MuzzleValidationException` in CI. | ||
|
|
||
| Move such constants to the helper class declared in `helperClassNames()`: | ||
|
|
||
| ```java | ||
| // Wrong -- in @RequiresRequestContext advice inner class | ||
| private static final int MAX_FILES = Config.get().getAppSecMaxFileContentCount(); | ||
|
|
||
| // Correct -- in NettyMultipartHelper or similar helper | ||
| public class NettyMultipartHelper { | ||
| static final int MAX_FILES_TO_INSPECT = Config.get().getAppSecMaxFileContentCount(); | ||
| } | ||
| ``` |
48 changes: 48 additions & 0 deletions
48
dd-java-agent/instrumentation/tomcat/tomcat-appsec/AGENTS.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Tomcat AppSec Instrumentation | ||
|
|
||
| Extended reference: [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md), | ||
| [docs/appsec/file-content.md](../../../../docs/appsec/file-content.md), | ||
| [docs/appsec/multipart-frameworks.md](../../../../docs/appsec/multipart-frameworks.md) | ||
|
|
||
| ## MUST call `effectivelyBlocked()` in Tomcat blocking path | ||
|
|
||
| Unlike Netty, Tomcat advice must call `effectivelyBlocked()` explicitly. The call must come AFTER | ||
| `t = new BlockingException(...)` and both must be inside `if (brf != null)`: | ||
|
|
||
| ```java | ||
| if (brf != null) { | ||
| brf.tryCommitBlockingResponse(ctx.getTraceSegment(), rba); | ||
| t = new BlockingException("..."); // 1. create first | ||
| ctx.getTraceSegment().effectivelyBlocked(); // 2. mark after | ||
| } | ||
| ``` | ||
|
|
||
| If `effectivelyBlocked()` throws (span already finished), the exception object must already exist | ||
| so the advice can still return a non-null value and let the container propagate it. | ||
|
|
||
| ## `Part` access via reflection: GlassFish restriction does NOT apply here | ||
|
|
||
| Tomcat's `ParameterCollector` uses reflection to avoid bytecode references to `javax` vs `jakarta` | ||
| Part types. This is safe for Tomcat because the agent classloader is not subject to Java 9+ module | ||
| access restrictions for Tomcat's classloader. | ||
|
|
||
| For GlassFish, direct cast via `javax.servlet.http.Part` interface is required instead. | ||
| See [docs/appsec/multipart-frameworks.md](../../../../docs/appsec/multipart-frameworks.md#glassfishpayara-no-reflection-via-parametercollector). | ||
|
|
||
| ## `inspectContent` flag must be evaluated before iterating parts | ||
|
|
||
| Check whether the `requestFilesContent` callback is registered BEFORE iterating parts, not inside | ||
| the loop. This avoids calling `getInputStream()` on every file when no rule uses `files_content`: | ||
|
|
||
| ```java | ||
| boolean inspectContent = cbp.getCallback(EVENTS.requestFilesContent()) != null; | ||
| ParameterCollector collector = new ParameterCollector.ParameterCollectorImpl(inspectContent); | ||
| for (Object part : parts) { | ||
| collector.addPart(part); | ||
| } | ||
| ``` | ||
|
|
||
| ## `helperClassNames()` must list all nested inner classes | ||
|
|
||
| ByteBuddy does not auto-discover nested classes. Every `$Inner` class used at runtime must be | ||
| explicitly listed in `helperClassNames()` or a `NoClassDefFoundError` will occur silently. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Undertow AppSec Instrumentation | ||
|
|
||
| Extended reference: [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md), | ||
| [docs/appsec/ig-events.md](../../../../docs/appsec/ig-events.md) | ||
|
|
||
| ## `tryCommitBlockingResponse` is not idempotent in Undertow | ||
|
|
||
| Unlike Tomcat/Jersey, Undertow's `tryCommitBlockingResponse` commits the response immediately. | ||
| Calling it twice sends a duplicate response and can throw an `IllegalStateException`. | ||
|
|
||
| When an advice has two blocking paths (body + filenames), the `t == null` guard must be checked | ||
| BEFORE each `tryCommitBlockingResponse` call, not after: | ||
|
|
||
| ```java | ||
| // Wrong -- commits response even if already blocked | ||
| if (bodyCallback != null && !map.isEmpty()) { | ||
| Flow<Void> flow = bodyCallback.apply(ctx, map); | ||
| t = tryBlock(flow, ctx); | ||
| } | ||
| if (filenamesCallback != null && !filenames.isEmpty()) { | ||
| Flow<Void> flow = filenamesCallback.apply(ctx, filenames); | ||
| t = tryBlock(flow, ctx); // tryBlock calls tryCommitBlockingResponse again | ||
| } | ||
|
|
||
| // Correct -- guard prevents second commit | ||
| if (bodyCallback != null && !map.isEmpty()) { | ||
| Flow<Void> flow = bodyCallback.apply(ctx, map); | ||
| t = tryBlock(flow, ctx); | ||
| } | ||
| if (t == null && filenamesCallback != null && !filenames.isEmpty()) { | ||
| Flow<Void> flow = filenamesCallback.apply(ctx, filenames); | ||
| t = tryBlock(flow, ctx); | ||
| } | ||
| ``` | ||
|
|
||
| See [docs/appsec/ig-events.md](../../../docs/appsec/ig-events.md#undertow-trycommitblockingresponse-is-not-idempotent) | ||
| for the full explanation of why this differs from other Servlet containers. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Vert.x Web AppSec Instrumentation | ||
|
|
||
| Extended reference: [docs/appsec/ig-events.md](../../../../docs/appsec/ig-events.md), | ||
| [docs/appsec/blocking-patterns.md](../../../../docs/appsec/blocking-patterns.md) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Missing any one causes a silent no-op-> Maybe we could add a test that checks all these 4 files are coherent in their registered addresses?Then we wouldn't need to document it at all. If you miss them, you get a test error explaining the issue.