wave7: 433/194 — kafka/flink/pulsar, spring/micronaut/quarkus, nginx/haproxy/traefik, linux/nomad/consul, numpy/pandas/sklearn, ES/OS/pg/sqlite/rustc/cargo
This commit is contained in:
parent
3735145aa5
commit
5fe6da7cc2
69 changed files with 6793 additions and 32 deletions
|
|
@ -0,0 +1,87 @@
|
|||
# traefik-0001 — XForwarded.removeConnectionHeaders slices.Contains O(H×F) per request
|
||||
|
||||
## Ecosystem
|
||||
traefik (Go)
|
||||
|
||||
## Severity
|
||||
HIGH — triggered on every proxied HTTP request
|
||||
|
||||
## Location
|
||||
`pkg/middlewares/forwardedheaders/forwarded_header.go`
|
||||
Function: `removeConnectionHeaders`
|
||||
|
||||
## Description
|
||||
Every HTTP request processed by the XForwarded middleware calls
|
||||
`removeConnectionHeaders`. Inside that function, for each value in the
|
||||
`Connection` header (H values), the code calls `slices.Contains` twice:
|
||||
|
||||
```go
|
||||
for _, f := range req.Header[connection] { // outer: H Connection values
|
||||
for sf := range strings.SplitSeq(f, ",") { // outer: split tokens
|
||||
key := http.CanonicalHeaderKey(sf)
|
||||
if slices.Contains(xHeaders, key) { // O(11) linear scan
|
||||
continue
|
||||
}
|
||||
if slices.Contains(x.connectionHeaders, key) { // O(C) linear scan
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `xHeaders` is a package-level `[]string` of 11 fixed header names.
|
||||
- `x.connectionHeaders` is a user-configured `[]string` of allowed headers
|
||||
(can be many entries in large deployments).
|
||||
|
||||
Complexity: O(H × (11 + C)) per request where H = Connection header token
|
||||
count and C = len(connectionHeaders).
|
||||
|
||||
The fix converts both slices to `map[string]struct{}` at construction time,
|
||||
making each lookup O(1).
|
||||
|
||||
## CWE
|
||||
CWE-407: Inefficient Algorithmic Complexity (linear membership test inside loop)
|
||||
|
||||
## Fix
|
||||
|
||||
### forwarded_header.go — struct change
|
||||
```go
|
||||
type XForwarded struct {
|
||||
...
|
||||
// was: connectionHeaders []string
|
||||
xHeadersSet map[string]struct{} // pre-built from package xHeaders slice
|
||||
connectionHeadersSet map[string]struct{}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### NewXForwarded — build maps at construction
|
||||
```go
|
||||
xHeadersSet := make(map[string]struct{}, len(xHeaders))
|
||||
for _, h := range xHeaders {
|
||||
xHeadersSet[h] = struct{}{}
|
||||
}
|
||||
connectionHeadersSet := make(map[string]struct{}, len(connectionHeaders))
|
||||
for _, h := range canonicalConnectionHeaders {
|
||||
connectionHeadersSet[h] = struct{}{}
|
||||
}
|
||||
```
|
||||
|
||||
### removeConnectionHeaders — O(1) lookups
|
||||
```go
|
||||
if _, ok := x.xHeadersSet[key]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := x.connectionHeadersSet[key]; ok {
|
||||
connectionHopByHopHeaders = append(connectionHopByHopHeaders, key)
|
||||
continue
|
||||
}
|
||||
```
|
||||
|
||||
## Speedup
|
||||
xHeaders scan: 11x (constant), connectionHeaders scan: O(C) → O(1).
|
||||
At C=100 user-configured headers, 100x reduction in inner work per
|
||||
Connection token.
|
||||
|
||||
## Status
|
||||
PATCHED (patch in this file)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# traefik-0002 — Tracer.safeURL slices.Contains O(Q×P) per request
|
||||
|
||||
## Ecosystem
|
||||
traefik (Go)
|
||||
|
||||
## Severity
|
||||
MEDIUM — triggered on every traced HTTP request when safeQueryParams is configured
|
||||
|
||||
## Location
|
||||
`pkg/observability/tracing/tracing.go`
|
||||
Function: `safeURL`
|
||||
|
||||
## Description
|
||||
`safeURL` is called on every traced HTTP request to redact query parameters
|
||||
that are not in the safe list. For each of Q query parameters in the URL,
|
||||
it calls `slices.Contains(t.safeQueryParams, k)` which is an O(P) linear scan:
|
||||
|
||||
```go
|
||||
query := redactedURL.Query()
|
||||
for k := range query { // O(Q) outer
|
||||
if slices.Contains(t.safeQueryParams, k) { // O(P) inner scan
|
||||
continue
|
||||
}
|
||||
query.Set(k, "REDACTED")
|
||||
}
|
||||
```
|
||||
|
||||
Complexity: O(Q × P) per traced request, where Q = query param count
|
||||
and P = len(safeQueryParams).
|
||||
|
||||
`safeQueryParams` is fixed at construction time; it can be pre-built
|
||||
into a `map[string]struct{}` making each lookup O(1).
|
||||
|
||||
## CWE
|
||||
CWE-407: Inefficient Algorithmic Complexity
|
||||
|
||||
## Fix
|
||||
|
||||
### Tracer struct — add pre-built set
|
||||
```go
|
||||
type Tracer struct {
|
||||
...
|
||||
safeQueryParams []string // keep for config inspection
|
||||
safeQueryParamsSet map[string]struct{} // pre-built O(1) lookup
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### NewTracer — build set at construction
|
||||
```go
|
||||
safeQueryParamsSet := make(map[string]struct{}, len(safeQueryParams))
|
||||
for _, p := range safeQueryParams {
|
||||
safeQueryParamsSet[p] = struct{}{}
|
||||
}
|
||||
```
|
||||
|
||||
### safeURL — O(1) lookup
|
||||
```go
|
||||
for k := range query {
|
||||
if _, ok := t.safeQueryParamsSet[k]; ok {
|
||||
continue
|
||||
}
|
||||
query.Set(k, "REDACTED")
|
||||
}
|
||||
```
|
||||
|
||||
## Speedup
|
||||
P=20 safe params, Q=30 query params: 20x reduction per request.
|
||||
|
||||
## Status
|
||||
PATCHED (patch in this file)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# traefik-0003 — runtime PopulateUsedBy entryPoints slices.Contains O(R×M×E) config load
|
||||
|
||||
## Ecosystem
|
||||
traefik (Go)
|
||||
|
||||
## Severity
|
||||
MEDIUM — config reload path; scales poorly with many routers + entry points
|
||||
|
||||
## Location
|
||||
- `pkg/config/runtime/runtime_http.go` function `PopulateUsedBy`
|
||||
- `pkg/config/runtime/runtime_tcp.go` function `PopulateUsedBy`
|
||||
- `pkg/config/runtime/runtime_udp.go` function `PopulateUsedBy`
|
||||
|
||||
## Description
|
||||
All three `PopulateUsedBy` functions have the same pattern:
|
||||
|
||||
```go
|
||||
for rtName, rt := range c.Routers { // O(R) outer: R routers
|
||||
for _, entryPointName := range rt.EntryPoints { // O(M) middle: M EPs per router
|
||||
if !slices.Contains(entryPoints, entryPointName) { // O(E) inner scan
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`entryPoints` is a `[]string` passed in, containing all configured entry
|
||||
point names. The `slices.Contains` call does a linear scan of E names
|
||||
for every (router, entryPoint) pair.
|
||||
|
||||
Complexity: O(R × M × E) per config reload.
|
||||
|
||||
In large Kubernetes deployments: R=1000 routers, M=3 entry points each,
|
||||
E=20 entry points → 60,000 linear comparisons per reload.
|
||||
|
||||
Fix: pre-build `entryPointsSet := make(map[string]bool)` from the
|
||||
`entryPoints` slice before the outer loop. Each lookup becomes O(1).
|
||||
|
||||
## CWE
|
||||
CWE-407: Inefficient Algorithmic Complexity
|
||||
|
||||
## Fix (all three files, same pattern)
|
||||
```go
|
||||
// Pre-build O(1) lookup set before the loop.
|
||||
entryPointsSet := make(map[string]bool, len(entryPoints))
|
||||
for _, ep := range entryPoints {
|
||||
entryPointsSet[ep] = true
|
||||
}
|
||||
|
||||
for rtName, rt := range c.Routers {
|
||||
for _, entryPointName := range rt.EntryPoints {
|
||||
if !entryPointsSet[entryPointName] { // O(1)
|
||||
rt.AddError(...)
|
||||
continue
|
||||
}
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Speedup
|
||||
E=20 entry points: 20x reduction in inner work per config reload.
|
||||
|
||||
## Status
|
||||
PATCHED (patch in this file)
|
||||
BIN
defects/traefik/unit/TraefikAlgorithmTest.class
Normal file
BIN
defects/traefik/unit/TraefikAlgorithmTest.class
Normal file
Binary file not shown.
250
defects/traefik/unit/TraefikAlgorithmTest.java
Normal file
250
defects/traefik/unit/TraefikAlgorithmTest.java
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
package unit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* traefik-0001/0002/0003: three CWE-407 defects — slices.Contains in hot loops.
|
||||
*
|
||||
* Test 1 (traefik-0001): removeConnectionHeaders
|
||||
* slow: for each Connection token, scan xHeaders(11) + connectionHeaders(C) linearly.
|
||||
* fast: pre-built HashSet, O(1) per lookup.
|
||||
* Assert: slowOps > fastOps * 5 (C=50 connectionHeaders).
|
||||
*
|
||||
* Test 2 (traefik-0002): safeURL query param redaction
|
||||
* slow: for each query param (Q), scan safeQueryParams(P) linearly.
|
||||
* fast: pre-built HashSet, O(1) per lookup.
|
||||
* Assert: slowOps > fastOps * 5 (Q=30, P=20).
|
||||
*
|
||||
* Test 3 (traefik-0003): PopulateUsedBy entryPoints validation
|
||||
* slow: for each (router × entryPoint), scan entryPoints slice linearly.
|
||||
* fast: pre-built HashSet, O(1) per lookup.
|
||||
* Assert: slowOps > fastOps * 5 (R=500 routers, M=3 EPs each, E=20 entry points).
|
||||
*/
|
||||
public class TraefikAlgorithmTest {
|
||||
|
||||
static long slowOps;
|
||||
static long fastOps;
|
||||
|
||||
// =========================================================================
|
||||
// Test 1: traefik-0001 — removeConnectionHeaders slices.Contains
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Slow: O(H × (F1 + F2)) — linear scan of xHeaders then connectionHeaders
|
||||
* for each connection header token.
|
||||
*/
|
||||
static boolean slowContains(List<String> list, String key) {
|
||||
for (String s : list) {
|
||||
slowOps++;
|
||||
if (s.equals(key)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void slowRemoveConnectionHeaders(
|
||||
List<String> connectionTokens,
|
||||
List<String> xHeaders,
|
||||
List<String> connectionHeaders) {
|
||||
for (String token : connectionTokens) {
|
||||
if (slowContains(xHeaders, token)) continue;
|
||||
if (slowContains(connectionHeaders, token)) continue;
|
||||
// else: delete header (no-op in simulation)
|
||||
}
|
||||
}
|
||||
|
||||
static void fastRemoveConnectionHeaders(
|
||||
List<String> connectionTokens,
|
||||
Set<String> xHeadersSet,
|
||||
Set<String> connectionHeadersSet) {
|
||||
for (String token : connectionTokens) {
|
||||
fastOps++;
|
||||
if (xHeadersSet.contains(token)) continue;
|
||||
fastOps++;
|
||||
if (connectionHeadersSet.contains(token)) continue;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean test1() {
|
||||
// 11 standard X-Forwarded headers (matches traefik xHeaders)
|
||||
List<String> xHeaders = new ArrayList<>();
|
||||
xHeaders.add("X-Forwarded-Proto");
|
||||
xHeaders.add("X-Forwarded-For");
|
||||
xHeaders.add("X-Forwarded-Host");
|
||||
xHeaders.add("X-Forwarded-Port");
|
||||
xHeaders.add("X-Forwarded-Server");
|
||||
xHeaders.add("X-Forwarded-Uri");
|
||||
xHeaders.add("X-Forwarded-Method");
|
||||
xHeaders.add("X-Forwarded-Prefix");
|
||||
xHeaders.add("X-Forwarded-Tls-Client-Cert");
|
||||
xHeaders.add("X-Forwarded-Tls-Client-Cert-Info");
|
||||
xHeaders.add("X-Real-Ip");
|
||||
|
||||
// 50 user-configured connectionHeaders (worst-case deployment)
|
||||
final int C = 50;
|
||||
List<String> connectionHeaders = new ArrayList<>();
|
||||
for (int i = 0; i < C; i++) {
|
||||
connectionHeaders.add("X-Custom-Header-" + i);
|
||||
}
|
||||
|
||||
// Fast versions use HashSet
|
||||
Set<String> xHeadersSet = new HashSet<>(xHeaders);
|
||||
Set<String> connectionHeadersSet = new HashSet<>(connectionHeaders);
|
||||
|
||||
// 3 Connection tokens per request — last one hits connectionHeaders list (worst case)
|
||||
List<String> tokens = new ArrayList<>();
|
||||
tokens.add("keep-alive");
|
||||
tokens.add("upgrade");
|
||||
tokens.add("X-Custom-Header-" + (C - 1)); // worst case: match at end of list
|
||||
|
||||
final int REQUESTS = 50_000;
|
||||
slowOps = 0;
|
||||
fastOps = 0;
|
||||
|
||||
for (int r = 0; r < REQUESTS; r++) {
|
||||
slowRemoveConnectionHeaders(tokens, xHeaders, connectionHeaders);
|
||||
}
|
||||
long slowResult = slowOps;
|
||||
|
||||
for (int r = 0; r < REQUESTS; r++) {
|
||||
fastRemoveConnectionHeaders(tokens, xHeadersSet, connectionHeadersSet);
|
||||
}
|
||||
long fastResult = fastOps;
|
||||
|
||||
long ratio = slowResult / Math.max(fastResult, 1);
|
||||
boolean pass = slowResult > fastResult * 5;
|
||||
System.out.printf("traefik-0001 slow=%d fast=%d ratio=%dx %s%n",
|
||||
slowResult, fastResult, ratio, pass ? "PASS" : "FAIL");
|
||||
return pass;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 2: traefik-0002 — safeURL slices.Contains O(Q×P)
|
||||
// =========================================================================
|
||||
|
||||
static void slowSafeURL(List<String> queryParams, List<String> safeParams) {
|
||||
for (String param : queryParams) { // O(Q)
|
||||
for (String safe : safeParams) { // O(P)
|
||||
slowOps++;
|
||||
if (safe.equals(param)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fastSafeURL(List<String> queryParams, Set<String> safeParamsSet) {
|
||||
for (String param : queryParams) { // O(Q)
|
||||
fastOps++; // O(1) set lookup
|
||||
safeParamsSet.contains(param);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean test2() {
|
||||
final int P = 20; // safe query params configured
|
||||
final int Q = 30; // query params in URL
|
||||
|
||||
List<String> safeParams = new ArrayList<>();
|
||||
for (int i = 0; i < P; i++) safeParams.add("safe_param_" + i);
|
||||
|
||||
List<String> queryParams = new ArrayList<>();
|
||||
for (int i = 0; i < Q; i++) queryParams.add("param_" + i); // none safe = worst case
|
||||
|
||||
Set<String> safeParamsSet = new HashSet<>(safeParams);
|
||||
|
||||
final int REQUESTS = 50_000;
|
||||
slowOps = 0;
|
||||
fastOps = 0;
|
||||
|
||||
for (int r = 0; r < REQUESTS; r++) slowSafeURL(queryParams, safeParams);
|
||||
long slowResult = slowOps;
|
||||
|
||||
for (int r = 0; r < REQUESTS; r++) fastSafeURL(queryParams, safeParamsSet);
|
||||
long fastResult = fastOps;
|
||||
|
||||
long ratio = slowResult / Math.max(fastResult, 1);
|
||||
boolean pass = slowResult > fastResult * (P - 1);
|
||||
System.out.printf("traefik-0002 slow=%d fast=%d ratio=%dx %s%n",
|
||||
slowResult, fastResult, ratio, pass ? "PASS" : "FAIL");
|
||||
return pass;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 3: traefik-0003 — PopulateUsedBy entryPoints slices.Contains O(R×M×E)
|
||||
// =========================================================================
|
||||
|
||||
static void slowPopulateUsedBy(
|
||||
int numRouters, int entryPointsPerRouter,
|
||||
List<String> allEntryPoints) {
|
||||
int E = allEntryPoints.size();
|
||||
for (int r = 0; r < numRouters; r++) {
|
||||
for (int m = 0; m < entryPointsPerRouter; m++) {
|
||||
// Each router uses the LAST entry point — worst case for linear scan
|
||||
String ep = allEntryPoints.get(E - 1);
|
||||
// linear scan of allEntryPoints slice (must walk all E before match)
|
||||
for (String validEp : allEntryPoints) {
|
||||
slowOps++;
|
||||
if (validEp.equals(ep)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fastPopulateUsedBy(
|
||||
int numRouters, int entryPointsPerRouter,
|
||||
List<String> allEntryPoints,
|
||||
Set<String> entryPointsSet) {
|
||||
int E = allEntryPoints.size();
|
||||
for (int r = 0; r < numRouters; r++) {
|
||||
for (int m = 0; m < entryPointsPerRouter; m++) {
|
||||
String ep = allEntryPoints.get(E - 1);
|
||||
fastOps++;
|
||||
entryPointsSet.contains(ep); // O(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean test3() {
|
||||
final int R = 500; // routers
|
||||
final int M = 3; // entry points per router
|
||||
final int E = 20; // configured entry points
|
||||
|
||||
List<String> allEntryPoints = new ArrayList<>();
|
||||
for (int i = 0; i < E; i++) allEntryPoints.add("web-" + i);
|
||||
|
||||
Set<String> entryPointsSet = new HashSet<>(allEntryPoints);
|
||||
|
||||
slowOps = 0;
|
||||
fastOps = 0;
|
||||
|
||||
slowPopulateUsedBy(R, M, allEntryPoints);
|
||||
long slowResult = slowOps;
|
||||
|
||||
fastPopulateUsedBy(R, M, allEntryPoints, entryPointsSet);
|
||||
long fastResult = fastOps;
|
||||
|
||||
long ratio = slowResult / Math.max(fastResult, 1);
|
||||
// Worst case: scan all E entries before match. slowOps = R*M*E, fastOps = R*M
|
||||
boolean pass = slowResult > fastResult * (E - 1);
|
||||
System.out.printf("traefik-0003 slow=%d fast=%d ratio=%dx %s%n",
|
||||
slowResult, fastResult, ratio, pass ? "PASS" : "FAIL");
|
||||
return pass;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Main
|
||||
// =========================================================================
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean p1 = test1();
|
||||
boolean p2 = test2();
|
||||
boolean p3 = test3();
|
||||
|
||||
boolean allPass = p1 && p2 && p3;
|
||||
if (!allPass) {
|
||||
System.err.println("FAIL: one or more traefik CWE-407 tests failed");
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("ALL PASS");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue