@sudobility/sider_lib 0.0.6 → 0.0.8
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/tokenize.js +18 -2
- package/dist/tokenize.test.js +43 -0
- package/package.json +2 -2
- package/src/tokenize.test.ts +66 -0
- package/src/tokenize.ts +23 -2
package/dist/tokenize.js
CHANGED
|
@@ -43,17 +43,33 @@ export function tokenizeObservation(raw, known) {
|
|
|
43
43
|
});
|
|
44
44
|
};
|
|
45
45
|
// 1. Known values (exact substring match everywhere).
|
|
46
|
+
//
|
|
47
|
+
// `known` is EVERY cookie and storage entry the page has, not the ones this
|
|
48
|
+
// request uses, so most of them appear nowhere in it. Masking still runs
|
|
49
|
+
// unconditionally — that costs nothing and protects a value wherever it turns
|
|
50
|
+
// up — but a SLOT is only proposed for a value actually found in the request.
|
|
51
|
+
//
|
|
52
|
+
// Minting one regardless, at a default of `authorization`, invented an
|
|
53
|
+
// injection point out of an absence of evidence: a public GET carried 21
|
|
54
|
+
// headers all named `authorization`, each holding a different analytics
|
|
55
|
+
// cookie, because every cookie on the page fell through to that default.
|
|
56
|
+
// Where a value goes is something to observe, never to assume.
|
|
46
57
|
for (const s of known) {
|
|
47
58
|
if (!s.value)
|
|
48
59
|
continue;
|
|
49
|
-
const loc = locate(raw, s.value)
|
|
60
|
+
const loc = locate(raw, s.value);
|
|
50
61
|
const ph = samplePlaceholder(s.slotId);
|
|
51
62
|
url = url.split(s.value).join(ph);
|
|
52
63
|
for (const k of Object.keys(headers))
|
|
53
64
|
headers[k] = headers[k].split(s.value).join(ph);
|
|
54
65
|
requestBody = replaceInJson(requestBody, s.value, ph);
|
|
55
66
|
responseBody = replaceInJson(responseBody, s.value, ph);
|
|
56
|
-
|
|
67
|
+
if (!loc)
|
|
68
|
+
continue;
|
|
69
|
+
// Found inside the Cookie header: the browser put it there, so it is
|
|
70
|
+
// attached automatically at execution rather than substituted by us.
|
|
71
|
+
const inCookieHeader = loc.at === "header" && loc.name.toLowerCase() === "cookie";
|
|
72
|
+
ensure(s.slotId, s.kind, inCookieHeader ? "auto_cookie" : s.role, inCookieHeader ? { at: "cookie", name: loc.name } : loc, s.resolverHint);
|
|
57
73
|
}
|
|
58
74
|
// 2. Credential-bearing headers (by name), even when the value wasn't in storage.
|
|
59
75
|
for (const name of Object.keys(headers)) {
|
package/dist/tokenize.test.js
CHANGED
|
@@ -40,3 +40,46 @@ test("leaves non-secret data untouched", () => {
|
|
|
40
40
|
const { observation } = tokenizeObservation(raw({ responseBody: { price: 400, section: "A1" } }), []);
|
|
41
41
|
expect(observation.responseBody).toEqual({ price: 400, section: "A1" });
|
|
42
42
|
});
|
|
43
|
+
// --- where a secret goes is observed, never assumed ------------------------
|
|
44
|
+
const analyticsCookie = (value = "GA1.2.9876") => ({
|
|
45
|
+
value,
|
|
46
|
+
slotId: "slot:www.usfca.edu:cookie_ga",
|
|
47
|
+
kind: "unknown",
|
|
48
|
+
role: "active_inject",
|
|
49
|
+
resolverHint: { from: "cookie", name: "_ga" },
|
|
50
|
+
});
|
|
51
|
+
test("proposes no slot for a page secret the request never carries", () => {
|
|
52
|
+
// Every cookie and storage entry on the page is offered as `known`, not the
|
|
53
|
+
// ones this request uses. Defaulting the absent ones to an Authorization
|
|
54
|
+
// header gave a public GET 21 headers all named `authorization`, each holding
|
|
55
|
+
// a different analytics cookie.
|
|
56
|
+
const { slots } = tokenizeObservation(raw(), [analyticsCookie()]);
|
|
57
|
+
expect(slots).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
test("still masks a value it proposes no slot for", () => {
|
|
60
|
+
// Absent from the REQUEST is not absent from the observation: the same value
|
|
61
|
+
// echoed in a response body must not be uploaded in the clear.
|
|
62
|
+
const { observation, slots } = tokenizeObservation(raw({ responseBody: { tracker: "GA1.2.9876" } }), [analyticsCookie()]);
|
|
63
|
+
expect(JSON.stringify(observation.responseBody)).not.toContain("GA1.2.9876");
|
|
64
|
+
expect(slots).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
test("proposes a slot where the value is actually found", () => {
|
|
67
|
+
const { slots } = tokenizeObservation(raw({ requestHeaders: { "x-session": "GA1.2.9876" } }), [analyticsCookie()]);
|
|
68
|
+
expect(slots).toHaveLength(1);
|
|
69
|
+
expect(slots[0].injectionLocation).toEqual({ at: "header", name: "x-session" });
|
|
70
|
+
expect(slots[0].role).toBe("active_inject");
|
|
71
|
+
});
|
|
72
|
+
test("finds a value in a query parameter", () => {
|
|
73
|
+
const { slots } = tokenizeObservation(raw({ url: "https://resale.fifa.com/api/seats?sid=GA1.2.9876" }), [analyticsCookie()]);
|
|
74
|
+
expect(slots[0].injectionLocation).toEqual({ at: "query", param: "sid" });
|
|
75
|
+
});
|
|
76
|
+
test("finds a value in the request body", () => {
|
|
77
|
+
const { slots } = tokenizeObservation(raw({ method: "POST", requestBody: { auth: { token: "GA1.2.9876" } } }), [analyticsCookie()]);
|
|
78
|
+
expect(slots[0].injectionLocation).toEqual({ at: "body", jsonPath: "auth.token" });
|
|
79
|
+
});
|
|
80
|
+
test("a value found in the Cookie header is attached by the browser, not injected", () => {
|
|
81
|
+
const { slots } = tokenizeObservation(raw({ requestHeaders: { cookie: "_ga=GA1.2.9876" } }), [analyticsCookie()]);
|
|
82
|
+
const slot = slots.find((s) => s.id === "slot:www.usfca.edu:cookie_ga");
|
|
83
|
+
expect(slot.role).toBe("auto_cookie");
|
|
84
|
+
expect(slot.injectionLocation.at).toBe("cookie");
|
|
85
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/sider_lib",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Sider business logic — recipe compiler, egress gates, tokenizer, clustering. Pure, no Chrome/DOM/server.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"publishConfig": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"test": "bun test"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@sudobility/sider_types": "^0.0.
|
|
22
|
+
"@sudobility/sider_types": "^0.0.10"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/bun": "^1.3.14",
|
package/src/tokenize.test.ts
CHANGED
|
@@ -52,3 +52,69 @@ test("leaves non-secret data untouched", () => {
|
|
|
52
52
|
const { observation } = tokenizeObservation(raw({ responseBody: { price: 400, section: "A1" } }), []);
|
|
53
53
|
expect(observation.responseBody).toEqual({ price: 400, section: "A1" });
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
// --- where a secret goes is observed, never assumed ------------------------
|
|
57
|
+
|
|
58
|
+
const analyticsCookie = (value = "GA1.2.9876"): KnownSecret => ({
|
|
59
|
+
value,
|
|
60
|
+
slotId: "slot:www.usfca.edu:cookie_ga",
|
|
61
|
+
kind: "unknown",
|
|
62
|
+
role: "active_inject",
|
|
63
|
+
resolverHint: { from: "cookie", name: "_ga" },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("proposes no slot for a page secret the request never carries", () => {
|
|
67
|
+
// Every cookie and storage entry on the page is offered as `known`, not the
|
|
68
|
+
// ones this request uses. Defaulting the absent ones to an Authorization
|
|
69
|
+
// header gave a public GET 21 headers all named `authorization`, each holding
|
|
70
|
+
// a different analytics cookie.
|
|
71
|
+
const { slots } = tokenizeObservation(raw(), [analyticsCookie()]);
|
|
72
|
+
expect(slots).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("still masks a value it proposes no slot for", () => {
|
|
76
|
+
// Absent from the REQUEST is not absent from the observation: the same value
|
|
77
|
+
// echoed in a response body must not be uploaded in the clear.
|
|
78
|
+
const { observation, slots } = tokenizeObservation(
|
|
79
|
+
raw({ responseBody: { tracker: "GA1.2.9876" } }),
|
|
80
|
+
[analyticsCookie()],
|
|
81
|
+
);
|
|
82
|
+
expect(JSON.stringify(observation.responseBody)).not.toContain("GA1.2.9876");
|
|
83
|
+
expect(slots).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("proposes a slot where the value is actually found", () => {
|
|
87
|
+
const { slots } = tokenizeObservation(
|
|
88
|
+
raw({ requestHeaders: { "x-session": "GA1.2.9876" } }),
|
|
89
|
+
[analyticsCookie()],
|
|
90
|
+
);
|
|
91
|
+
expect(slots).toHaveLength(1);
|
|
92
|
+
expect(slots[0]!.injectionLocation).toEqual({ at: "header", name: "x-session" });
|
|
93
|
+
expect(slots[0]!.role).toBe("active_inject");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("finds a value in a query parameter", () => {
|
|
97
|
+
const { slots } = tokenizeObservation(
|
|
98
|
+
raw({ url: "https://resale.fifa.com/api/seats?sid=GA1.2.9876" }),
|
|
99
|
+
[analyticsCookie()],
|
|
100
|
+
);
|
|
101
|
+
expect(slots[0]!.injectionLocation).toEqual({ at: "query", param: "sid" });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("finds a value in the request body", () => {
|
|
105
|
+
const { slots } = tokenizeObservation(
|
|
106
|
+
raw({ method: "POST", requestBody: { auth: { token: "GA1.2.9876" } } }),
|
|
107
|
+
[analyticsCookie()],
|
|
108
|
+
);
|
|
109
|
+
expect(slots[0]!.injectionLocation).toEqual({ at: "body", jsonPath: "auth.token" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a value found in the Cookie header is attached by the browser, not injected", () => {
|
|
113
|
+
const { slots } = tokenizeObservation(
|
|
114
|
+
raw({ requestHeaders: { cookie: "_ga=GA1.2.9876" } }),
|
|
115
|
+
[analyticsCookie()],
|
|
116
|
+
);
|
|
117
|
+
const slot = slots.find((s) => s.id === "slot:www.usfca.edu:cookie_ga");
|
|
118
|
+
expect(slot!.role).toBe("auto_cookie");
|
|
119
|
+
expect(slot!.injectionLocation.at).toBe("cookie");
|
|
120
|
+
});
|
package/src/tokenize.ts
CHANGED
|
@@ -88,15 +88,36 @@ export function tokenizeObservation(raw: RawObservation, known: KnownSecret[]):
|
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
// 1. Known values (exact substring match everywhere).
|
|
91
|
+
//
|
|
92
|
+
// `known` is EVERY cookie and storage entry the page has, not the ones this
|
|
93
|
+
// request uses, so most of them appear nowhere in it. Masking still runs
|
|
94
|
+
// unconditionally — that costs nothing and protects a value wherever it turns
|
|
95
|
+
// up — but a SLOT is only proposed for a value actually found in the request.
|
|
96
|
+
//
|
|
97
|
+
// Minting one regardless, at a default of `authorization`, invented an
|
|
98
|
+
// injection point out of an absence of evidence: a public GET carried 21
|
|
99
|
+
// headers all named `authorization`, each holding a different analytics
|
|
100
|
+
// cookie, because every cookie on the page fell through to that default.
|
|
101
|
+
// Where a value goes is something to observe, never to assume.
|
|
91
102
|
for (const s of known) {
|
|
92
103
|
if (!s.value) continue;
|
|
93
|
-
const loc = locate(raw, s.value)
|
|
104
|
+
const loc = locate(raw, s.value);
|
|
94
105
|
const ph = samplePlaceholder(s.slotId);
|
|
95
106
|
url = url.split(s.value).join(ph);
|
|
96
107
|
for (const k of Object.keys(headers)) headers[k] = headers[k]!.split(s.value).join(ph);
|
|
97
108
|
requestBody = replaceInJson(requestBody, s.value, ph);
|
|
98
109
|
responseBody = replaceInJson(responseBody, s.value, ph);
|
|
99
|
-
|
|
110
|
+
if (!loc) continue;
|
|
111
|
+
// Found inside the Cookie header: the browser put it there, so it is
|
|
112
|
+
// attached automatically at execution rather than substituted by us.
|
|
113
|
+
const inCookieHeader = loc.at === "header" && loc.name.toLowerCase() === "cookie";
|
|
114
|
+
ensure(
|
|
115
|
+
s.slotId,
|
|
116
|
+
s.kind,
|
|
117
|
+
inCookieHeader ? "auto_cookie" : s.role,
|
|
118
|
+
inCookieHeader ? { at: "cookie", name: loc.name } : loc,
|
|
119
|
+
s.resolverHint,
|
|
120
|
+
);
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
// 2. Credential-bearing headers (by name), even when the value wasn't in storage.
|