Commit 80d6ce3
authored
feat(AAP): add support for sub applications in FastAPI (#17570)
APPSEC-62352
## Description
Add Application & API Protection (AAP) support for FastAPI and Starlette applications using mounted sub-applications (`app.mount()`).
Previously, when endpoints were defined on sub-applications, several issues occurred:
- **Duplicate WAF invocations**: The WAF ran on both the intermediate `Mount` handler and the final `Route` handler, producing duplicate triggers.
- **Duplicate tracing**: Each sub-app's `TraceMiddleware` created redundant processing (body parsing, distributed tracing activation, WAF dispatch).
- **Incomplete path parameters**: `scope["path_params"]` was only partially populated when ASM processed the `Mount` handler.
- **Incorrect endpoint discovery**: Routes in sub-apps were registered with their local paths (e.g., `/get`) instead of the full mounted path (e.g., `/api/v2/get`).
### Changes
**`ddtrace/contrib/internal/asgi/middleware.py`**
- Detect sub-app middleware via `is_subapp = "datadog" in scope`. Sub-app middlewares still create spans (for trace visibility), but skip body parsing, distributed tracing activation, and WAF dispatch to avoid duplicates.
- Trigger endpoint discovery via route tree walk on first request (root middleware only).
**`ddtrace/contrib/internal/starlette/patch.py`**
- Gate ASM/WAF processing behind `isinstance(instance, starlette.routing.Route)` so it only runs on the final `Route` handler, not intermediate `Mount` handlers.
- Replace init-time endpoint registration with `_collect_routes_from_app()`, a recursive route tree walker that accumulates mount prefixes for correct full-path registration.
- Handle `Host`-based routing in the tree walker.
- Per-route error handling in the walker to prevent one bad route from blocking discovery.
**`ddtrace/appsec/_asm_request_context.py`**
- Guard `start_context()` to skip creating a new ASM environment when `is_subapp=True` and an ASM context already exists from the parent. This preserves the parent's request data (body, headers, etc.) for the WAF instead of creating an empty child context that would shadow it.
**Tests**
- Parametrize the FastAPI appsec test suite with both flat (`get_app`) and sub-app (`get_app_with_subapps`) app variants via the `interface` fixture.
- Add `app_subapps.py` with endpoints grouped into mounted sub-applications.
- Reset `endpoint_collection` singleton between parametrized variants to prevent state leakage.
## Testing
All test suites pass with both flat and sub-app variants:
- `appsec_threats_fastapi_no_iast`: 2006 passed
- `appsec_threats_fastapi_iast`: 2006 passed
- `appsec_threats_fastapi_rc`: 10 passed
- `contrib::fastapi`: 72 passed (snapshot tests unchanged)
- `contrib::starlette`: 52 passed (snapshot tests unchanged)
- `contrib::asgi`: 183 passed
## Risks
- **Endpoint discovery timing**: Endpoint registration is now deferred to first request (was at `Route.__init__` time). This is necessary because mount prefixes are unknown at init time. The app tree is guaranteed complete at first request for all standard ASGI server setups.
- **Trace shape**: Sub-app spans are preserved (child `starlette.request`/`fastapi.request` spans still appear), but they no longer carry body/query data or trigger WAF independently. Existing snapshot tests pass without changes.
- **`is_subapp` propagation**: The flag is passed via `core.context_with_data` and checked in `start_context()`. Only affects mounted sub-apps where `scope["datadog"]` is already set by the parent middleware.
## Additional Notes
- The `is_subapp` approach was chosen over a full middleware skip to preserve sub-app child spans in traces, maintaining backward compatibility with existing snapshot tests and regular monitoring behavior.
Co-authored-by: christophe.papazian <christophe.papazian@datadoghq.com>1 parent d02755c commit 80d6ce3
6 files changed
Lines changed: 683 additions & 61 deletions
File tree
- ddtrace
- appsec
- contrib/internal
- asgi
- starlette
- releasenotes/notes
- tests/appsec/contrib_appsec
- fastapi_app
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
576 | 576 | | |
577 | 577 | | |
578 | 578 | | |
579 | | - | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
580 | 585 | | |
581 | 586 | | |
582 | 587 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
198 | 220 | | |
199 | 221 | | |
200 | 222 | | |
| |||
207 | 229 | | |
208 | 230 | | |
209 | 231 | | |
210 | | - | |
211 | | - | |
212 | | - | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
213 | 236 | | |
214 | 237 | | |
215 | 238 | | |
| |||
233 | 256 | | |
234 | 257 | | |
235 | 258 | | |
| 259 | + | |
236 | 260 | | |
237 | 261 | | |
238 | 262 | | |
| |||
281 | 305 | | |
282 | 306 | | |
283 | 307 | | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
284 | 311 | | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
299 | 327 | | |
300 | 328 | | |
301 | 329 | | |
302 | 330 | | |
303 | 331 | | |
304 | 332 | | |
305 | 333 | | |
306 | | - | |
307 | | - | |
| 334 | + | |
| 335 | + | |
308 | 336 | | |
309 | 337 | | |
310 | 338 | | |
| |||
474 | 502 | | |
475 | 503 | | |
476 | 504 | | |
477 | | - | |
| 505 | + | |
| 506 | + | |
478 | 507 | | |
479 | 508 | | |
480 | 509 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
94 | 84 | | |
95 | 85 | | |
96 | 86 | | |
97 | 87 | | |
98 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
99 | 127 | | |
100 | 128 | | |
101 | 129 | | |
| |||
189 | 217 | | |
190 | 218 | | |
191 | 219 | | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
215 | 247 | | |
216 | 248 | | |
217 | 249 | | |
| |||
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
0 commit comments