@sudobility/sider_lib 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/correlation.d.ts +2 -0
- package/dist/correlation.js +28 -0
- package/dist/execute.js +15 -1
- package/dist/gates.test.js +21 -0
- package/dist/tokenize.js +6 -0
- package/package.json +1 -1
- package/src/correlation.ts +30 -0
- package/src/execute.ts +15 -1
- package/src/gates.test.ts +26 -0
- package/src/tokenize.ts +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Headers that identify a REQUEST, not a person.
|
|
2
|
+
//
|
|
3
|
+
// `x-ebay-c-correlation-session` is a per-page correlation id: it ties a page's
|
|
4
|
+
// calls together in eBay's own logs and carries no authority whatsoever. It was
|
|
5
|
+
// treated as a credential because its name ends in "session", and the fallout
|
|
6
|
+
// was worse than noise — nothing local holds such a value, so egress could not
|
|
7
|
+
// resolve it, the call was blocked, and the agent told a signed-in user to sign
|
|
8
|
+
// in. The actual eBay session is a cookie the browser attaches by itself.
|
|
9
|
+
//
|
|
10
|
+
// A correlation header is safe to omit. The server mints its own when it is
|
|
11
|
+
// missing, which is exactly what happens on a request the site makes itself.
|
|
12
|
+
/** Names that mark a header as request-scoped tracing rather than a credential. */
|
|
13
|
+
const CORRELATION_RE = /correlation|traceparent|tracestate|\btrace\b|request[-_]?id|\breq[-_]?id\b|\bspan\b|\bray\b|\bb3\b/i;
|
|
14
|
+
/**
|
|
15
|
+
* Names that are unambiguously credentials, whatever else they contain.
|
|
16
|
+
*
|
|
17
|
+
* Checked first so a hypothetical `x-csrf-correlation` stays a secret. Weak
|
|
18
|
+
* words like "session", "token" and "id" are deliberately NOT here: those are
|
|
19
|
+
* the ones correlation headers routinely contain, and treating them as decisive
|
|
20
|
+
* is the mistake being fixed.
|
|
21
|
+
*/
|
|
22
|
+
const CREDENTIAL_RE = /authorization|bearer|csrf|xsrf|api[-_]?key|apikey|secret|password/i;
|
|
23
|
+
/** Whether a header names request tracing that can be dropped rather than resolved. */
|
|
24
|
+
export function isCorrelationHeader(name) {
|
|
25
|
+
if (CREDENTIAL_RE.test(name))
|
|
26
|
+
return false;
|
|
27
|
+
return CORRELATION_RE.test(name);
|
|
28
|
+
}
|
package/dist/execute.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// TokenMap) and substitutes it into the sentinel positions the gates already
|
|
5
5
|
// proved are confined to the declared location.
|
|
6
6
|
import { compileRecipe, secretSentinel } from "./recipe";
|
|
7
|
+
import { isCorrelationHeader } from "./correlation";
|
|
7
8
|
import { evaluateGates } from "./gates";
|
|
8
9
|
export function detokenizeRequest(req, resolve, slotsById) {
|
|
9
10
|
let url = req.url;
|
|
@@ -15,8 +16,21 @@ export function detokenizeRequest(req, resolve, slotsById) {
|
|
|
15
16
|
if (slot && (slot.role === "auto_cookie" || slot.injectionLocation.at === "cookie"))
|
|
16
17
|
continue;
|
|
17
18
|
const value = resolve(slotId);
|
|
18
|
-
if (value === undefined)
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
// A correlation header that cannot be resolved is dropped, not fatal.
|
|
21
|
+
// Slots minted before these were recognised still sit in the registry,
|
|
22
|
+
// and every one of them would otherwise block a call the user's own
|
|
23
|
+
// session can make — the site issues a fresh id when the header is absent.
|
|
24
|
+
if (slot && slot.injectionLocation.at === "header" && isCorrelationHeader(slot.injectionLocation.name)) {
|
|
25
|
+
delete headers[slot.injectionLocation.name];
|
|
26
|
+
const sen = secretSentinel(slotId);
|
|
27
|
+
url = url.split(sen).join("");
|
|
28
|
+
if (bodyStr !== undefined)
|
|
29
|
+
bodyStr = bodyStr.split(sen).join("");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
19
32
|
throw new Error(`Unresolved secret slot at egress: ${slotId}`);
|
|
33
|
+
}
|
|
20
34
|
const sen = secretSentinel(slotId);
|
|
21
35
|
url = url.split(sen).join(value);
|
|
22
36
|
for (const k of Object.keys(headers))
|
package/dist/gates.test.js
CHANGED
|
@@ -109,3 +109,24 @@ test("cookie-borne slot is refused (must be auto-attached, never injected)", ()
|
|
|
109
109
|
if (!out.ok)
|
|
110
110
|
expect(out.gate).toBe("injection_location");
|
|
111
111
|
});
|
|
112
|
+
// --- correlation headers ----------------------------------------------------
|
|
113
|
+
import { isCorrelationHeader } from "./correlation";
|
|
114
|
+
test("a correlation id is not a credential", () => {
|
|
115
|
+
// The header that blocked a signed-in user's call: it ends in "session", so
|
|
116
|
+
// the name rule claimed it, and no local store holds a per-page trace id.
|
|
117
|
+
expect(isCorrelationHeader("x-ebay-c-correlation-session")).toBe(true);
|
|
118
|
+
expect(isCorrelationHeader("x-request-id")).toBe(true);
|
|
119
|
+
expect(isCorrelationHeader("traceparent")).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
test("real credentials are still credentials, whatever else the name says", () => {
|
|
122
|
+
expect(isCorrelationHeader("authorization")).toBe(false);
|
|
123
|
+
expect(isCorrelationHeader("cookie")).toBe(false);
|
|
124
|
+
expect(isCorrelationHeader("x-csrf-token")).toBe(false);
|
|
125
|
+
expect(isCorrelationHeader("x-api-key")).toBe(false);
|
|
126
|
+
// Contains a correlation word AND an unambiguous credential word: the
|
|
127
|
+
// credential wins, because dropping it would send an unauthenticated call.
|
|
128
|
+
expect(isCorrelationHeader("x-csrf-correlation")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
test("a session header with no correlation marker stays a secret", () => {
|
|
131
|
+
expect(isCorrelationHeader("x-session-id")).toBe(false);
|
|
132
|
+
});
|
package/dist/tokenize.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 3. Body fields whose KEY looks credential-bearing (e.g. access_token).
|
|
11
11
|
// 4. JWT-shaped strings anywhere in a body.
|
|
12
12
|
// Bias is conservative-for-privacy: over-mask rather than leak.
|
|
13
|
+
import { isCorrelationHeader } from "./correlation";
|
|
13
14
|
export function samplePlaceholder(slotId) {
|
|
14
15
|
return `{{secret:${slotId}}}`;
|
|
15
16
|
}
|
|
@@ -78,6 +79,11 @@ export function tokenizeObservation(raw, known) {
|
|
|
78
79
|
continue;
|
|
79
80
|
if (!SECRET_HEADER_RE.test(name))
|
|
80
81
|
continue;
|
|
82
|
+
// A correlation id is not a credential. Minting a slot for one guarantees a
|
|
83
|
+
// block later: no local store holds a per-page trace id, so the call dies
|
|
84
|
+
// at egress for want of a value the site does not need sent.
|
|
85
|
+
if (isCorrelationHeader(name))
|
|
86
|
+
continue;
|
|
81
87
|
const lname = name.toLowerCase();
|
|
82
88
|
const slotId = `slot:${host}:${normalize(name)}`;
|
|
83
89
|
headers[name] = samplePlaceholder(slotId);
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Headers that identify a REQUEST, not a person.
|
|
2
|
+
//
|
|
3
|
+
// `x-ebay-c-correlation-session` is a per-page correlation id: it ties a page's
|
|
4
|
+
// calls together in eBay's own logs and carries no authority whatsoever. It was
|
|
5
|
+
// treated as a credential because its name ends in "session", and the fallout
|
|
6
|
+
// was worse than noise — nothing local holds such a value, so egress could not
|
|
7
|
+
// resolve it, the call was blocked, and the agent told a signed-in user to sign
|
|
8
|
+
// in. The actual eBay session is a cookie the browser attaches by itself.
|
|
9
|
+
//
|
|
10
|
+
// A correlation header is safe to omit. The server mints its own when it is
|
|
11
|
+
// missing, which is exactly what happens on a request the site makes itself.
|
|
12
|
+
|
|
13
|
+
/** Names that mark a header as request-scoped tracing rather than a credential. */
|
|
14
|
+
const CORRELATION_RE = /correlation|traceparent|tracestate|\btrace\b|request[-_]?id|\breq[-_]?id\b|\bspan\b|\bray\b|\bb3\b/i;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Names that are unambiguously credentials, whatever else they contain.
|
|
18
|
+
*
|
|
19
|
+
* Checked first so a hypothetical `x-csrf-correlation` stays a secret. Weak
|
|
20
|
+
* words like "session", "token" and "id" are deliberately NOT here: those are
|
|
21
|
+
* the ones correlation headers routinely contain, and treating them as decisive
|
|
22
|
+
* is the mistake being fixed.
|
|
23
|
+
*/
|
|
24
|
+
const CREDENTIAL_RE = /authorization|bearer|csrf|xsrf|api[-_]?key|apikey|secret|password/i;
|
|
25
|
+
|
|
26
|
+
/** Whether a header names request tracing that can be dropped rather than resolved. */
|
|
27
|
+
export function isCorrelationHeader(name: string): boolean {
|
|
28
|
+
if (CREDENTIAL_RE.test(name)) return false;
|
|
29
|
+
return CORRELATION_RE.test(name);
|
|
30
|
+
}
|
package/src/execute.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { CompiledRequest, SecretSlot } from "@sudobility/sider_types";
|
|
8
8
|
import { compileRecipe, secretSentinel } from "./recipe";
|
|
9
|
+
import { isCorrelationHeader } from "./correlation";
|
|
9
10
|
import { evaluateGates } from "./gates";
|
|
10
11
|
import type { ToolSpec } from "@sudobility/sider_types";
|
|
11
12
|
import type { CompileContext } from "./recipe";
|
|
@@ -37,7 +38,20 @@ export function detokenizeRequest(
|
|
|
37
38
|
if (slot && (slot.role === "auto_cookie" || slot.injectionLocation.at === "cookie")) continue;
|
|
38
39
|
|
|
39
40
|
const value = resolve(slotId);
|
|
40
|
-
if (value === undefined)
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
// A correlation header that cannot be resolved is dropped, not fatal.
|
|
43
|
+
// Slots minted before these were recognised still sit in the registry,
|
|
44
|
+
// and every one of them would otherwise block a call the user's own
|
|
45
|
+
// session can make — the site issues a fresh id when the header is absent.
|
|
46
|
+
if (slot && slot.injectionLocation.at === "header" && isCorrelationHeader(slot.injectionLocation.name)) {
|
|
47
|
+
delete headers[slot.injectionLocation.name];
|
|
48
|
+
const sen = secretSentinel(slotId);
|
|
49
|
+
url = url.split(sen).join("");
|
|
50
|
+
if (bodyStr !== undefined) bodyStr = bodyStr.split(sen).join("");
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Unresolved secret slot at egress: ${slotId}`);
|
|
54
|
+
}
|
|
41
55
|
|
|
42
56
|
const sen = secretSentinel(slotId);
|
|
43
57
|
url = url.split(sen).join(value);
|
package/src/gates.test.ts
CHANGED
|
@@ -122,3 +122,29 @@ test("cookie-borne slot is refused (must be auto-attached, never injected)", ()
|
|
|
122
122
|
expect(out.ok).toBe(false);
|
|
123
123
|
if (!out.ok) expect(out.gate).toBe("injection_location");
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
// --- correlation headers ----------------------------------------------------
|
|
127
|
+
|
|
128
|
+
import { isCorrelationHeader } from "./correlation";
|
|
129
|
+
|
|
130
|
+
test("a correlation id is not a credential", () => {
|
|
131
|
+
// The header that blocked a signed-in user's call: it ends in "session", so
|
|
132
|
+
// the name rule claimed it, and no local store holds a per-page trace id.
|
|
133
|
+
expect(isCorrelationHeader("x-ebay-c-correlation-session")).toBe(true);
|
|
134
|
+
expect(isCorrelationHeader("x-request-id")).toBe(true);
|
|
135
|
+
expect(isCorrelationHeader("traceparent")).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("real credentials are still credentials, whatever else the name says", () => {
|
|
139
|
+
expect(isCorrelationHeader("authorization")).toBe(false);
|
|
140
|
+
expect(isCorrelationHeader("cookie")).toBe(false);
|
|
141
|
+
expect(isCorrelationHeader("x-csrf-token")).toBe(false);
|
|
142
|
+
expect(isCorrelationHeader("x-api-key")).toBe(false);
|
|
143
|
+
// Contains a correlation word AND an unambiguous credential word: the
|
|
144
|
+
// credential wins, because dropping it would send an unauthenticated call.
|
|
145
|
+
expect(isCorrelationHeader("x-csrf-correlation")).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a session header with no correlation marker stays a secret", () => {
|
|
149
|
+
expect(isCorrelationHeader("x-session-id")).toBe(false);
|
|
150
|
+
});
|
package/src/tokenize.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// 4. JWT-shaped strings anywhere in a body.
|
|
12
12
|
// Bias is conservative-for-privacy: over-mask rather than leak.
|
|
13
13
|
|
|
14
|
+
import { isCorrelationHeader } from "./correlation";
|
|
14
15
|
import type {
|
|
15
16
|
HttpMethod,
|
|
16
17
|
InjectionLocation,
|
|
@@ -125,6 +126,10 @@ export function tokenizeObservation(raw: RawObservation, known: KnownSecret[]):
|
|
|
125
126
|
const val = headers[name];
|
|
126
127
|
if (typeof val !== "string" || !val || val.startsWith("{{secret:")) continue;
|
|
127
128
|
if (!SECRET_HEADER_RE.test(name)) continue;
|
|
129
|
+
// A correlation id is not a credential. Minting a slot for one guarantees a
|
|
130
|
+
// block later: no local store holds a per-page trace id, so the call dies
|
|
131
|
+
// at egress for want of a value the site does not need sent.
|
|
132
|
+
if (isCorrelationHeader(name)) continue;
|
|
128
133
|
const lname = name.toLowerCase();
|
|
129
134
|
const slotId = `slot:${host}:${normalize(name)}`;
|
|
130
135
|
headers[name] = samplePlaceholder(slotId);
|