@tellescope/sdk 1.251.0 → 1.252.1
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/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.js +139 -0
- package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js +337 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js +287 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js +30 -20
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +234 -198
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +349 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +247 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +278 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +201 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js +148 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js +88 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js +237 -0
- package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts +38 -0
- package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js +222 -0
- package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js +301 -0
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +198 -151
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.js +135 -0
- package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.js.map +1 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js +333 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +280 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/integrations_redacted.test.js +30 -20
- package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -1
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +235 -199
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +345 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +243 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +271 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +194 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js +144 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js +84 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js +233 -0
- package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts +38 -0
- package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js +218 -0
- package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js.map +1 -0
- package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/user_portal_settings.test.js +297 -0
- package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +198 -151
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/calendar_event_webhook_template.test.ts +204 -0
- package/src/tests/api_tests/enduser_login_rate_limits.test.ts +178 -0
- package/src/tests/api_tests/integrations_redacted.test.ts +8 -0
- package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +113 -88
- package/src/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.ts +236 -0
- package/src/tests/api_tests/security/F-0005-ai-conversations-rbac.test.ts +154 -0
- package/src/tests/api_tests/security/F-0007-invite-user-enumeration.test.ts +198 -0
- package/src/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.ts +130 -0
- package/src/tests/api_tests/security/F-0013-sanitize-user-html.test.ts +109 -0
- package/src/tests/api_tests/security/F-0016-prototype-pollution.test.ts +50 -0
- package/src/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.ts +161 -0
- package/src/tests/api_tests/security/F-0076-self-admin-role-assignment.test.ts +165 -0
- package/src/tests/api_tests/user_portal_settings.test.ts +217 -0
- package/src/tests/tests.ts +25 -2
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.sanitize_user_html_xss_tests = void 0;
|
|
40
|
+
var utilities_1 = require("@tellescope/utilities");
|
|
41
|
+
// Regression test for F-0013 / F-0014 (pattern 06 — XSS via dangerouslySetInnerHTML).
|
|
42
|
+
// sanitize_user_html is the canonical render-time sanitizer that replaced remove_script_tags
|
|
43
|
+
// at every dangerouslySetInnerHTML sink. This asserts it neutralizes XSS vectors (incl. encoded /
|
|
44
|
+
// whitespace / mixed-case / iframe-srcdoc bypass variants) while preserving legitimate
|
|
45
|
+
// customization HTML (tables, headings, lists, links, images, inline styles).
|
|
46
|
+
//
|
|
47
|
+
// Pure-function test — no Session needed. Runs as part of the main suite and standalone:
|
|
48
|
+
// ./build_cjs.sh && cd packages/public/sdk && node -r dotenv/config lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js
|
|
49
|
+
var fail = function (msg) { throw new Error(msg); };
|
|
50
|
+
var has_no_executable_vector = function (out) {
|
|
51
|
+
var o = out.toLowerCase();
|
|
52
|
+
// A handler smuggled into an attribute VALUE (e.g. title="<img onerror=...>") is inert
|
|
53
|
+
// text — strip quoted values before checking for *live* on*= attributes to avoid false positives.
|
|
54
|
+
var withoutValues = o.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''");
|
|
55
|
+
return !/\son[a-z]+\s*=/.test(withoutValues) // no live on*= event-handler attribute
|
|
56
|
+
&& !o.includes('javascript:') // dropped schemes never appear in safe output
|
|
57
|
+
&& !o.includes('vbscript:')
|
|
58
|
+
&& !o.includes('<script') // literal dangerous tags (encoded <script is fine)
|
|
59
|
+
&& !o.includes('<iframe')
|
|
60
|
+
&& !o.includes('<svg')
|
|
61
|
+
&& !o.includes('<math')
|
|
62
|
+
&& !o.includes('<object')
|
|
63
|
+
&& !o.includes('<embed')
|
|
64
|
+
&& !o.includes('<form')
|
|
65
|
+
&& !o.includes('<noscript')
|
|
66
|
+
&& !o.includes('<template');
|
|
67
|
+
};
|
|
68
|
+
var sanitize_user_html_xss_tests = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
69
|
+
var xssPayloads, _i, xssPayloads_1, _a, name_1, payload, out, clobber, heading, table, list, link, img, dataimg, fmt, mixed;
|
|
70
|
+
return __generator(this, function (_b) {
|
|
71
|
+
console.log("Running F-0013/F-0014 sanitize_user_html XSS regression tests");
|
|
72
|
+
xssPayloads = [
|
|
73
|
+
['img onerror', "<img src=x onerror=\"alert(document.domain)\">"],
|
|
74
|
+
['svg onload', "<svg onload=\"alert(1)\"></svg>"],
|
|
75
|
+
['svg animate onbegin', "<svg><animate onbegin=\"alert(1)\" attributeName=\"x\" dur=\"1s\"></svg>"],
|
|
76
|
+
['details ontoggle', "<details open ontoggle=\"alert(1)\"></details>"],
|
|
77
|
+
['input onfocus autofocus', "<input autofocus onfocus=\"alert(1)\">"],
|
|
78
|
+
['body onpageshow', "<body onpageshow=\"alert(1)\">"],
|
|
79
|
+
['a javascript scheme', "<a href=\"javascript:alert(1)\">x</a>"],
|
|
80
|
+
['a javascript entity-encoded', "<a href=\"jav	ascript:alert(1)\">x</a>"],
|
|
81
|
+
['iframe javascript src', "<iframe src=\"javascript:alert(1)\"></iframe>"],
|
|
82
|
+
['iframe srcdoc nested', "<iframe srcdoc=\"<img src=x onerror=alert(1)>\"></iframe>"],
|
|
83
|
+
['script tag', "<script>alert(1)</script>"],
|
|
84
|
+
['onerror newline before =', "<img src=x onerror\n=\"alert(1)\">"],
|
|
85
|
+
['onerror mixed case', "<IMG SRC=x OnErRoR=\"alert(1)\">"],
|
|
86
|
+
['marquee onstart', "<marquee onstart=\"alert(1)\">x</marquee>"],
|
|
87
|
+
// mutation / namespace confusion — svg/math/noscript/template must be stripped
|
|
88
|
+
['mathml mglyph style mxss', "<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(1)>"],
|
|
89
|
+
['svg foreignObject', "<svg><foreignObject><img src=x onerror=alert(1)></foreignObject></svg>"],
|
|
90
|
+
['noscript context confusion', "<noscript><p title=\"</noscript><img src=x onerror=alert(1)>\">"],
|
|
91
|
+
['template content', "<template><img src=x onerror=alert(1)></template>"],
|
|
92
|
+
// comment / CDATA confusion
|
|
93
|
+
['comment confusion', "<!--><img src=x onerror=alert(1)>-->"],
|
|
94
|
+
['cdata confusion', "<![CDATA[<img src=x onerror=alert(1)>]]>"],
|
|
95
|
+
// markup smuggled inside an attribute value must stay inert
|
|
96
|
+
['markup inside attr value', "<img src=\"x\" alt=\"<script>alert(1)</script>\">"],
|
|
97
|
+
// protocol obfuscation
|
|
98
|
+
['vbscript scheme', "<a href=\"vbscript:msgbox(1)\">x</a>"],
|
|
99
|
+
['data text/html href', "<a href=\"data:text/html,<script>alert(1)</script>\">x</a>"],
|
|
100
|
+
['javascript decimal entity', "<a href=\"Javascript:alert(1)\">x</a>"],
|
|
101
|
+
['javascript newline entity', "<a href=\"jav
ascript:alert(1)\">x</a>"],
|
|
102
|
+
];
|
|
103
|
+
for (_i = 0, xssPayloads_1 = xssPayloads; _i < xssPayloads_1.length; _i++) {
|
|
104
|
+
_a = xssPayloads_1[_i], name_1 = _a[0], payload = _a[1];
|
|
105
|
+
out = (0, utilities_1.sanitize_user_html)(payload);
|
|
106
|
+
if (!has_no_executable_vector(out))
|
|
107
|
+
fail("XSS not neutralized [".concat(name_1, "] -> ").concat(out));
|
|
108
|
+
}
|
|
109
|
+
clobber = (0, utilities_1.sanitize_user_html)("<a id=\"x\" name=\"getElementById\">link</a><img name=\"y\">");
|
|
110
|
+
if (/\b(id|name)\s*=/.test(clobber))
|
|
111
|
+
fail("id/name not stripped (DOM clobbering): ".concat(clobber));
|
|
112
|
+
heading = (0, utilities_1.sanitize_user_html)("<h1>Welcome</h1><h3 style=\"color:#333\">Sub</h3>");
|
|
113
|
+
if (!(heading.includes('<h1>') && heading.includes('<h3') && heading.toLowerCase().includes('color')))
|
|
114
|
+
fail("headings/style stripped: ".concat(heading));
|
|
115
|
+
table = (0, utilities_1.sanitize_user_html)("<table><thead><tr><th>H</th></tr></thead><tbody><tr><td style=\"padding:4px\" colspan=\"2\">cell</td></tr></tbody></table>");
|
|
116
|
+
if (!(table.includes('<table') && table.includes('<td') && table.includes('colspan')))
|
|
117
|
+
fail("table stripped: ".concat(table));
|
|
118
|
+
list = (0, utilities_1.sanitize_user_html)("<ul><li>a</li></ul><ol start=\"3\"><li>c</li></ol>");
|
|
119
|
+
if (!(list.includes('<ul') && list.includes('<li') && list.includes('<ol')))
|
|
120
|
+
fail("list stripped: ".concat(list));
|
|
121
|
+
link = (0, utilities_1.sanitize_user_html)("<a href=\"https://example.com\">link</a>");
|
|
122
|
+
if (!link.includes('href="https://example.com"'))
|
|
123
|
+
fail("safe link stripped: ".concat(link));
|
|
124
|
+
if (!link.toLowerCase().includes('noopener'))
|
|
125
|
+
fail("external link not hardened: ".concat(link));
|
|
126
|
+
img = (0, utilities_1.sanitize_user_html)("<img src=\"https://cdn.example.com/a.png\" alt=\"pic\" width=\"200\">");
|
|
127
|
+
if (!(img.includes('src="https://cdn.example.com/a.png"') && img.includes('alt="pic"')))
|
|
128
|
+
fail("http image stripped: ".concat(img));
|
|
129
|
+
dataimg = (0, utilities_1.sanitize_user_html)("<img src=\"data:image/png;base64,iVBORw0KGgo=\">");
|
|
130
|
+
if (!dataimg.includes('data:image/png'))
|
|
131
|
+
fail("data: image stripped: ".concat(dataimg));
|
|
132
|
+
fmt = (0, utilities_1.sanitize_user_html)("<p><strong>b</strong> <em>i</em> <span style=\"font-size:14px\">s</span></p><blockquote>q</blockquote>");
|
|
133
|
+
if (!(fmt.includes('<strong>') && fmt.includes('<span') && fmt.toLowerCase().includes('font-size')))
|
|
134
|
+
fail("formatting stripped: ".concat(fmt));
|
|
135
|
+
mixed = (0, utilities_1.sanitize_user_html)("<p>Hello <b>name</b></p><img src=x onerror=\"steal()\">");
|
|
136
|
+
if (!(mixed.includes('<b>name</b>') && !/\son[a-z]+\s*=/.test(mixed.toLowerCase())))
|
|
137
|
+
fail("mixed content not handled: ".concat(mixed));
|
|
138
|
+
console.log("✅ F-0013/F-0014 sanitize_user_html XSS regression tests passed");
|
|
139
|
+
return [2 /*return*/];
|
|
140
|
+
});
|
|
141
|
+
}); };
|
|
142
|
+
exports.sanitize_user_html_xss_tests = sanitize_user_html_xss_tests;
|
|
143
|
+
if (require.main === module) {
|
|
144
|
+
(0, exports.sanitize_user_html_xss_tests)()
|
|
145
|
+
.then(function () { console.log("✅ suite completed"); process.exit(0); })
|
|
146
|
+
.catch(function (err) { console.error("❌ suite failed:", err); process.exit(1); });
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=F-0013-sanitize-user-html.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"F-0013-sanitize-user-html.test.js","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0013-sanitize-user-html.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"F-0016-prototype-pollution.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0016-prototype-pollution.test.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,yBAAyB,qBAgCrC,CAAA"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.prototype_pollution_tests = void 0;
|
|
40
|
+
var utilities_1 = require("@tellescope/utilities");
|
|
41
|
+
// Regression test for F-0016 (pattern 17 — prototype pollution).
|
|
42
|
+
// add_value_for_dotted_key must NOT write through __proto__/constructor/prototype path segments
|
|
43
|
+
// (which would pollute Object.prototype process-wide), while still performing legitimate dotted assignment.
|
|
44
|
+
//
|
|
45
|
+
// Pure-function test — no Session needed. Runs in the main suite and standalone:
|
|
46
|
+
// ./build_cjs.sh && cd packages/public/sdk && node -r dotenv/config lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js
|
|
47
|
+
var fail = function (msg) { throw new Error(msg); };
|
|
48
|
+
var prototype_pollution_tests = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
49
|
+
var leakedA, leakedB, leakedC, obj, flat;
|
|
50
|
+
return __generator(this, function (_a) {
|
|
51
|
+
console.log("Running F-0016 prototype-pollution regression tests");
|
|
52
|
+
// 1. __proto__ path must not pollute Object.prototype
|
|
53
|
+
(0, utilities_1.add_value_for_dotted_key)({ insurance: {} }, 'insurance.__proto__.__pp_a__', 'polluted');
|
|
54
|
+
leakedA = {}.__pp_a__;
|
|
55
|
+
delete Object.prototype.__pp_a__; // clean up regardless, so a failure here can't contaminate the rest of the suite
|
|
56
|
+
if (leakedA !== undefined)
|
|
57
|
+
fail('Object.prototype polluted via __proto__ path');
|
|
58
|
+
// 2. constructor.prototype path must not pollute
|
|
59
|
+
(0, utilities_1.add_value_for_dotted_key)({ insurance: {} }, 'insurance.constructor.prototype.__pp_b__', 'polluted');
|
|
60
|
+
leakedB = {}.__pp_b__;
|
|
61
|
+
delete Object.prototype.__pp_b__;
|
|
62
|
+
if (leakedB !== undefined)
|
|
63
|
+
fail('Object.prototype polluted via constructor.prototype path');
|
|
64
|
+
// 3. a leading __proto__ segment must not pollute either
|
|
65
|
+
(0, utilities_1.add_value_for_dotted_key)({}, '__proto__.__pp_c__', 'polluted');
|
|
66
|
+
leakedC = {}.__pp_c__;
|
|
67
|
+
delete Object.prototype.__pp_c__;
|
|
68
|
+
if (leakedC !== undefined)
|
|
69
|
+
fail('Object.prototype polluted via leading __proto__ segment');
|
|
70
|
+
obj = { a: { b: {} } };
|
|
71
|
+
(0, utilities_1.add_value_for_dotted_key)(obj, 'a.b.c', 42);
|
|
72
|
+
if (obj.a.b.c !== 42)
|
|
73
|
+
fail('legitimate dotted assignment broke');
|
|
74
|
+
flat = {};
|
|
75
|
+
(0, utilities_1.add_value_for_dotted_key)(flat, 'name', 'ok');
|
|
76
|
+
if (flat.name !== 'ok')
|
|
77
|
+
fail('single-key assignment broke');
|
|
78
|
+
console.log("✅ F-0016 prototype-pollution regression tests passed");
|
|
79
|
+
return [2 /*return*/];
|
|
80
|
+
});
|
|
81
|
+
}); };
|
|
82
|
+
exports.prototype_pollution_tests = prototype_pollution_tests;
|
|
83
|
+
if (require.main === module) {
|
|
84
|
+
(0, exports.prototype_pollution_tests)()
|
|
85
|
+
.then(function () { console.log("✅ suite completed"); process.exit(0); })
|
|
86
|
+
.catch(function (err) { console.error("❌ suite failed:", err); process.exit(1); });
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=F-0016-prototype-pollution.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"F-0016-prototype-pollution.test.js","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0016-prototype-pollution.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mDAAgE;AAEhE,iEAAiE;AACjE,gGAAgG;AAChG,4GAA4G;AAC5G,EAAE;AACF,iFAAiF;AACjF,0IAA0I;AAE1I,IAAM,IAAI,GAAG,UAAC,GAAW,IAAO,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,CAAA,CAAC,CAAC,CAAA;AAE/C,IAAM,yBAAyB,GAAG;;;QACvC,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAA;QAElE,sDAAsD;QACtD,IAAA,oCAAwB,EAAC,EAAE,SAAS,EAAE,EAAE,EAAS,EAAE,8BAA8B,EAAE,UAAU,CAAC,CAAA;QACxF,OAAO,GAAI,EAAU,CAAC,QAAQ,CAAA;QACpC,OAAQ,MAAM,CAAC,SAAiB,CAAC,QAAQ,CAAA,CAAC,iFAAiF;QAC3H,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,8CAA8C,CAAC,CAAA;QAE/E,iDAAiD;QACjD,IAAA,oCAAwB,EAAC,EAAE,SAAS,EAAE,EAAE,EAAS,EAAE,0CAA0C,EAAE,UAAU,CAAC,CAAA;QACpG,OAAO,GAAI,EAAU,CAAC,QAAQ,CAAA;QACpC,OAAQ,MAAM,CAAC,SAAiB,CAAC,QAAQ,CAAA;QACzC,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,0DAA0D,CAAC,CAAA;QAE3F,yDAAyD;QACzD,IAAA,oCAAwB,EAAC,EAAS,EAAE,oBAAoB,EAAE,UAAU,CAAC,CAAA;QAC/D,OAAO,GAAI,EAAU,CAAC,QAAQ,CAAA;QACpC,OAAQ,MAAM,CAAC,SAAiB,CAAC,QAAQ,CAAA;QACzC,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,yDAAyD,CAAC,CAAA;QAGpF,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAS,CAAA;QACnC,IAAA,oCAAwB,EAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;QAC1C,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE;YAAE,IAAI,CAAC,oCAAoC,CAAC,CAAA;QAG1D,IAAI,GAAG,EAAS,CAAA;QACtB,IAAA,oCAAwB,EAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;QAC5C,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,IAAI,CAAC,6BAA6B,CAAC,CAAA;QAE3D,OAAO,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAA;;;KACpE,CAAA;AAhCY,QAAA,yBAAyB,6BAgCrC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE;IAC3B,IAAA,iCAAyB,GAAE;SACxB,IAAI,CAAC,cAAQ,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAC;SACjE,KAAK,CAAC,UAAC,GAAG,IAAO,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;CAC9E"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Session } from "../../../sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Tenant-boundary guard for cascade_role_rename (relates to security-audit finding F-0053,
|
|
4
|
+
* which was investigated and closed as a FALSE POSITIVE — see that file for the code trace).
|
|
5
|
+
*
|
|
6
|
+
* The `cascade_role_rename` side-effect handler
|
|
7
|
+
* ([event_handlers_v2/role_based_access_permissions.ts](packages/private/api/api/v1/event_handlers_v2/role_based_access_permissions.ts))
|
|
8
|
+
* runs when a `role_based_access_permissions` doc's `role` field changes. It finds every user
|
|
9
|
+
* with the old role name (via `DBUnrestricted.users`) and rewrites their `roles` array, then
|
|
10
|
+
* deauthenticates them. F-0053 hypothesized this query was globally cross-tenant. It is NOT:
|
|
11
|
+
* `DBUnrestricted` bypasses per-user/per-role RBAC but is STILL scoped to the acting session's
|
|
12
|
+
* `businessId` (see `modifyFilterForAccessConstraint` injecting `{ businessId }` at
|
|
13
|
+
* database.ts:1761-1763, reached via the `unrestricted: true` branch at database.ts:2137-2144).
|
|
14
|
+
*
|
|
15
|
+
* This test locks that boundary in place so a future refactor of `DBUnrestricted` semantics
|
|
16
|
+
* can't silently turn the cascade into a cross-tenant write:
|
|
17
|
+
* 1. Tenant A creates a role `ROLE_OLD` and assigns it to a Tenant A user (positive control).
|
|
18
|
+
* 2. Tenant B (separate businessId) has a user whose roles include the SAME `ROLE_OLD`.
|
|
19
|
+
* 3. Tenant A renames the role `ROLE_OLD` -> `ROLE_NEW`.
|
|
20
|
+
* 4. Assert the Tenant B user's roles are UNCHANGED (still `[ROLE_OLD]`) <-- guards the tenant boundary.
|
|
21
|
+
* 5. Assert the Tenant A user's roles ARE renamed to `[ROLE_NEW]` <-- same-tenant cascade works.
|
|
22
|
+
*
|
|
23
|
+
* Expected on current (correct) code: BOTH assertions pass. A regression that drops the
|
|
24
|
+
* `businessId` scoping would flip assertion #4 to red (Tenant B user becomes `[ROLE_NEW]`).
|
|
25
|
+
*
|
|
26
|
+
* A collision-proof unique role name (timestamped) is used so the test never touches real roles.
|
|
27
|
+
*/
|
|
28
|
+
export declare const cascade_role_rename_cross_tenant_tests: ({ sdk }: {
|
|
29
|
+
sdk: Session;
|
|
30
|
+
sdkNonAdmin: Session;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
//# sourceMappingURL=F-0053-cascade-role-rename-cross-tenant.test.d.ts.map
|
package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"F-0053-cascade-role-rename-cross-tenant.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAetC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,sCAAsC;SAA2B,OAAO;iBAAe,OAAO;mBA+F1G,CAAA"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.cascade_role_rename_cross_tenant_tests = void 0;
|
|
51
|
+
require('source-map-support').install();
|
|
52
|
+
var sdk_1 = require("../../../sdk");
|
|
53
|
+
var testing_1 = require("@tellescope/testing");
|
|
54
|
+
var setup_1 = require("../../setup");
|
|
55
|
+
var constants_1 = require("@tellescope/constants");
|
|
56
|
+
var host = process.env.API_URL || 'http://localhost:8080';
|
|
57
|
+
// Separate tenant (different businessId), reusing the same hardcoded apiKey that
|
|
58
|
+
// multi_tenant_tests relies on in tests.ts.
|
|
59
|
+
var OTHER_TENANT_API_KEY = "ba745e25162bb95a795c5fa1af70df188d93c4d3aac9c48b34a5c8c9dd7b80f7";
|
|
60
|
+
/**
|
|
61
|
+
* Tenant-boundary guard for cascade_role_rename (relates to security-audit finding F-0053,
|
|
62
|
+
* which was investigated and closed as a FALSE POSITIVE — see that file for the code trace).
|
|
63
|
+
*
|
|
64
|
+
* The `cascade_role_rename` side-effect handler
|
|
65
|
+
* ([event_handlers_v2/role_based_access_permissions.ts](packages/private/api/api/v1/event_handlers_v2/role_based_access_permissions.ts))
|
|
66
|
+
* runs when a `role_based_access_permissions` doc's `role` field changes. It finds every user
|
|
67
|
+
* with the old role name (via `DBUnrestricted.users`) and rewrites their `roles` array, then
|
|
68
|
+
* deauthenticates them. F-0053 hypothesized this query was globally cross-tenant. It is NOT:
|
|
69
|
+
* `DBUnrestricted` bypasses per-user/per-role RBAC but is STILL scoped to the acting session's
|
|
70
|
+
* `businessId` (see `modifyFilterForAccessConstraint` injecting `{ businessId }` at
|
|
71
|
+
* database.ts:1761-1763, reached via the `unrestricted: true` branch at database.ts:2137-2144).
|
|
72
|
+
*
|
|
73
|
+
* This test locks that boundary in place so a future refactor of `DBUnrestricted` semantics
|
|
74
|
+
* can't silently turn the cascade into a cross-tenant write:
|
|
75
|
+
* 1. Tenant A creates a role `ROLE_OLD` and assigns it to a Tenant A user (positive control).
|
|
76
|
+
* 2. Tenant B (separate businessId) has a user whose roles include the SAME `ROLE_OLD`.
|
|
77
|
+
* 3. Tenant A renames the role `ROLE_OLD` -> `ROLE_NEW`.
|
|
78
|
+
* 4. Assert the Tenant B user's roles are UNCHANGED (still `[ROLE_OLD]`) <-- guards the tenant boundary.
|
|
79
|
+
* 5. Assert the Tenant A user's roles ARE renamed to `[ROLE_NEW]` <-- same-tenant cascade works.
|
|
80
|
+
*
|
|
81
|
+
* Expected on current (correct) code: BOTH assertions pass. A regression that drops the
|
|
82
|
+
* `businessId` scoping would flip assertion #4 to red (Tenant B user becomes `[ROLE_NEW]`).
|
|
83
|
+
*
|
|
84
|
+
* A collision-proof unique role name (timestamped) is used so the test never touches real roles.
|
|
85
|
+
*/
|
|
86
|
+
var cascade_role_rename_cross_tenant_tests = function (_a) {
|
|
87
|
+
var sdk = _a.sdk;
|
|
88
|
+
return __awaiter(void 0, void 0, void 0, function () {
|
|
89
|
+
var stamp, ROLE_OLD, ROLE_NEW, sdkOther, rbapId, controlUserId, victimUserId, tenantABusinessId, rbap, controlUser, victimUser, victimBefore, controlBefore, victimAfter, controlAfter, _b, _c, _d;
|
|
90
|
+
return __generator(this, function (_e) {
|
|
91
|
+
switch (_e.label) {
|
|
92
|
+
case 0:
|
|
93
|
+
(0, testing_1.log_header)("F-0053: cascade role rename cross-tenant regression");
|
|
94
|
+
stamp = Date.now();
|
|
95
|
+
ROLE_OLD = "XTenantRename_".concat(stamp);
|
|
96
|
+
ROLE_NEW = "".concat(ROLE_OLD, "_renamed");
|
|
97
|
+
sdkOther = new sdk_1.Session({ host: host, apiKey: OTHER_TENANT_API_KEY });
|
|
98
|
+
_e.label = 1;
|
|
99
|
+
case 1:
|
|
100
|
+
_e.trys.push([1, , 13, 26]);
|
|
101
|
+
tenantABusinessId = sdk.userInfo.businessId;
|
|
102
|
+
return [4 /*yield*/, sdk.api.role_based_access_permissions.createOne({
|
|
103
|
+
role: ROLE_OLD,
|
|
104
|
+
permissions: __assign({}, constants_1.PROVIDER_PERMISSIONS),
|
|
105
|
+
})];
|
|
106
|
+
case 2:
|
|
107
|
+
rbap = _e.sent();
|
|
108
|
+
rbapId = rbap.id;
|
|
109
|
+
return [4 /*yield*/, sdk.api.users.createOne({
|
|
110
|
+
email: "f0053-control-".concat(stamp, "@example.com"),
|
|
111
|
+
notificationEmailsDisabled: true,
|
|
112
|
+
})];
|
|
113
|
+
case 3:
|
|
114
|
+
controlUser = _e.sent();
|
|
115
|
+
controlUserId = controlUser.id;
|
|
116
|
+
return [4 /*yield*/, sdk.api.users.updateOne(controlUserId, { roles: [ROLE_OLD] }, { replaceObjectFields: true })
|
|
117
|
+
// 3. Tenant B: create a throwaway victim user holding the SAME ROLE_OLD.
|
|
118
|
+
];
|
|
119
|
+
case 4:
|
|
120
|
+
_e.sent();
|
|
121
|
+
return [4 /*yield*/, sdkOther.api.users.createOne({
|
|
122
|
+
email: "f0053-victim-".concat(stamp, "@example.com"),
|
|
123
|
+
notificationEmailsDisabled: true,
|
|
124
|
+
})];
|
|
125
|
+
case 5:
|
|
126
|
+
victimUser = _e.sent();
|
|
127
|
+
victimUserId = victimUser.id;
|
|
128
|
+
return [4 /*yield*/, sdkOther.api.users.updateOne(victimUserId, { roles: [ROLE_OLD] }, { replaceObjectFields: true })
|
|
129
|
+
// 4. Setup sanity: tenants are distinct and both users actually hold ROLE_OLD before the rename.
|
|
130
|
+
// (Distinguishes a setup failure from the security assertion below.)
|
|
131
|
+
];
|
|
132
|
+
case 6:
|
|
133
|
+
_e.sent();
|
|
134
|
+
// 4. Setup sanity: tenants are distinct and both users actually hold ROLE_OLD before the rename.
|
|
135
|
+
// (Distinguishes a setup failure from the security assertion below.)
|
|
136
|
+
(0, testing_1.assert)(victimUser.businessId !== tenantABusinessId, "Victim user shares businessId with Tenant A (".concat(victimUser.businessId, ") \u2014 not a cross-tenant scenario"), 'F-0053 setup: tenants are distinct');
|
|
137
|
+
return [4 /*yield*/, sdkOther.api.users.getOne(victimUserId)];
|
|
138
|
+
case 7:
|
|
139
|
+
victimBefore = _e.sent();
|
|
140
|
+
return [4 /*yield*/, sdk.api.users.getOne(controlUserId)];
|
|
141
|
+
case 8:
|
|
142
|
+
controlBefore = _e.sent();
|
|
143
|
+
(0, testing_1.assert)(JSON.stringify(victimBefore.roles) === JSON.stringify([ROLE_OLD])
|
|
144
|
+
&& JSON.stringify(controlBefore.roles) === JSON.stringify([ROLE_OLD]), "Setup failed: expected both users to hold [".concat(ROLE_OLD, "] (victim=").concat(JSON.stringify(victimBefore.roles), ", control=").concat(JSON.stringify(controlBefore.roles), ")"), 'F-0053 setup: both users hold ROLE_OLD');
|
|
145
|
+
// 5. Tenant A renames the role. This triggers cascade_role_rename.
|
|
146
|
+
return [4 /*yield*/, sdk.api.role_based_access_permissions.updateOne(rbapId, { role: ROLE_NEW })];
|
|
147
|
+
case 9:
|
|
148
|
+
// 5. Tenant A renames the role. This triggers cascade_role_rename.
|
|
149
|
+
_e.sent();
|
|
150
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)
|
|
151
|
+
// 6. SECURITY ASSERTION — the Tenant B victim must be untouched by Tenant A's rename.
|
|
152
|
+
]; // let the side effect run
|
|
153
|
+
case 10:
|
|
154
|
+
_e.sent(); // let the side effect run
|
|
155
|
+
return [4 /*yield*/, sdkOther.api.users.getOne(victimUserId)];
|
|
156
|
+
case 11:
|
|
157
|
+
victimAfter = _e.sent();
|
|
158
|
+
(0, testing_1.assert)(JSON.stringify(victimAfter.roles) === JSON.stringify([ROLE_OLD]), "CROSS-TENANT LEAK: Tenant B victim roles changed to ".concat(JSON.stringify(victimAfter.roles), " ")
|
|
159
|
+
+ "after Tenant A renamed its role. Expected [".concat(ROLE_OLD, "]."), 'F-0053: Tenant B user roles unaffected by other-tenant role rename');
|
|
160
|
+
return [4 /*yield*/, sdk.api.users.getOne(controlUserId)];
|
|
161
|
+
case 12:
|
|
162
|
+
controlAfter = _e.sent();
|
|
163
|
+
(0, testing_1.assert)(JSON.stringify(controlAfter.roles) === JSON.stringify([ROLE_NEW]), "Same-tenant cascade broken: Tenant A control roles are ".concat(JSON.stringify(controlAfter.roles), ", ")
|
|
164
|
+
+ "expected [".concat(ROLE_NEW, "]."), 'F-0053: Tenant A user roles renamed by same-tenant cascade');
|
|
165
|
+
return [3 /*break*/, 26];
|
|
166
|
+
case 13:
|
|
167
|
+
if (!victimUserId) return [3 /*break*/, 17];
|
|
168
|
+
_e.label = 14;
|
|
169
|
+
case 14:
|
|
170
|
+
_e.trys.push([14, 16, , 17]);
|
|
171
|
+
return [4 /*yield*/, sdkOther.api.users.deleteOne(victimUserId)];
|
|
172
|
+
case 15:
|
|
173
|
+
_e.sent();
|
|
174
|
+
return [3 /*break*/, 17];
|
|
175
|
+
case 16:
|
|
176
|
+
_b = _e.sent();
|
|
177
|
+
return [3 /*break*/, 17];
|
|
178
|
+
case 17:
|
|
179
|
+
if (!controlUserId) return [3 /*break*/, 21];
|
|
180
|
+
_e.label = 18;
|
|
181
|
+
case 18:
|
|
182
|
+
_e.trys.push([18, 20, , 21]);
|
|
183
|
+
return [4 /*yield*/, sdk.api.users.deleteOne(controlUserId)];
|
|
184
|
+
case 19:
|
|
185
|
+
_e.sent();
|
|
186
|
+
return [3 /*break*/, 21];
|
|
187
|
+
case 20:
|
|
188
|
+
_c = _e.sent();
|
|
189
|
+
return [3 /*break*/, 21];
|
|
190
|
+
case 21:
|
|
191
|
+
if (!rbapId) return [3 /*break*/, 25];
|
|
192
|
+
_e.label = 22;
|
|
193
|
+
case 22:
|
|
194
|
+
_e.trys.push([22, 24, , 25]);
|
|
195
|
+
return [4 /*yield*/, sdk.api.role_based_access_permissions.deleteOne(rbapId)];
|
|
196
|
+
case 23:
|
|
197
|
+
_e.sent();
|
|
198
|
+
return [3 /*break*/, 25];
|
|
199
|
+
case 24:
|
|
200
|
+
_d = _e.sent();
|
|
201
|
+
return [3 /*break*/, 25];
|
|
202
|
+
case 25: return [7 /*endfinally*/];
|
|
203
|
+
case 26: return [2 /*return*/];
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
exports.cascade_role_rename_cross_tenant_tests = cascade_role_rename_cross_tenant_tests;
|
|
209
|
+
// Allow running this test file independently
|
|
210
|
+
if (require.main === module) {
|
|
211
|
+
console.log("\uD83C\uDF10 Using API URL: ".concat(host));
|
|
212
|
+
var sdk_2 = new sdk_1.Session({ host: host });
|
|
213
|
+
var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
|
|
214
|
+
var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
215
|
+
return __generator(this, function (_a) {
|
|
216
|
+
switch (_a.label) {
|
|
217
|
+
case 0: return [4 /*yield*/, (0, setup_1.setup_tests)(sdk_2, sdkNonAdmin_1)];
|
|
218
|
+
case 1:
|
|
219
|
+
_a.sent();
|
|
220
|
+
return [4 /*yield*/, (0, exports.cascade_role_rename_cross_tenant_tests)({ sdk: sdk_2, sdkNonAdmin: sdkNonAdmin_1 })];
|
|
221
|
+
case 2:
|
|
222
|
+
_a.sent();
|
|
223
|
+
return [2 /*return*/];
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}); };
|
|
227
|
+
runTests()
|
|
228
|
+
.then(function () {
|
|
229
|
+
console.log("✅ F-0053 cascade role rename cross-tenant test suite completed successfully");
|
|
230
|
+
process.exit(0);
|
|
231
|
+
})
|
|
232
|
+
.catch(function (error) {
|
|
233
|
+
console.error("❌ F-0053 cascade role rename cross-tenant test suite failed:", error);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=F-0053-cascade-role-rename-cross-tenant.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"F-0053-cascade-role-rename-cross-tenant.test.js","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Session } from "../../../sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Self-role privilege-escalation guard (relates to security-audit finding F-0076, which was
|
|
4
|
+
* investigated and closed as a FALSE POSITIVE — see that file for the full code trace).
|
|
5
|
+
*
|
|
6
|
+
* F-0076 hypothesized that a non-admin staff user could `PATCH /v1/users/{their-own-id}` with
|
|
7
|
+
* `{ roles: ['Admin'] }` and self-promote to Admin, because the FIRST `users` relationship
|
|
8
|
+
* constraint ("Only admin users can set the admin role",
|
|
9
|
+
* [schema.ts:3446](packages/public/schema/src/schema.ts#L3446)) has a self-exception
|
|
10
|
+
* (`if (_id === session.id) return`).
|
|
11
|
+
*
|
|
12
|
+
* That analysis missed the SECOND constraint, "Only admin users can update user roles"
|
|
13
|
+
* ([schema.ts:3486](packages/public/schema/src/schema.ts#L3486)), which has NO self-exception.
|
|
14
|
+
* Relationship constraints are AND-evaluated — `validateRelationshipConstraints`
|
|
15
|
+
* ([routing.ts:1240-1252](packages/private/api/api/modules/routing.ts#L1240)) loops the whole
|
|
16
|
+
* array and throws 400 on the FIRST evaluator that returns a string. So a non-admin self-update
|
|
17
|
+
* that includes `roles` passes constraint #1 (self-exception) but is rejected by constraint #2.
|
|
18
|
+
* The self-promotion is blocked.
|
|
19
|
+
*
|
|
20
|
+
* This test locks that boundary in place so a future refactor of the role constraints can't
|
|
21
|
+
* silently reintroduce the escalation. A dedicated throwaway non-admin user is used as the
|
|
22
|
+
* "attacker" (we never mutate the shared sdkNonAdmin's roles):
|
|
23
|
+
* 1. Admin creates a throwaway user and assigns it a non-admin role (`['Provider']`).
|
|
24
|
+
* 2. Authenticate AS that user via a freshly-minted auth token.
|
|
25
|
+
* 3. As the attacker, attempt four self-role mutations — ['Admin'], ['Provider','Admin'],
|
|
26
|
+
* an arbitrary role, and [] — and assert EACH is blocked. <-- the security assertions
|
|
27
|
+
* 4. Confirm (as admin) the attacker's roles are still ['Provider'] — nothing slipped through.
|
|
28
|
+
* 5. Positive control: admin CAN update the throwaway user's roles. <-- guards against an
|
|
29
|
+
* over-restrictive regression that would block legitimate admin role management.
|
|
30
|
+
*
|
|
31
|
+
* Expected on current (correct) code: all assertions pass. A regression that made the self-update
|
|
32
|
+
* path role-writable by non-admins would flip the step-3/step-4 assertions to red.
|
|
33
|
+
*/
|
|
34
|
+
export declare const self_admin_role_assignment_tests: ({ sdk }: {
|
|
35
|
+
sdk: Session;
|
|
36
|
+
sdkNonAdmin: Session;
|
|
37
|
+
}) => Promise<void>;
|
|
38
|
+
//# sourceMappingURL=F-0076-self-admin-role-assignment.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"F-0076-self-admin-role-assignment.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0076-self-admin-role-assignment.test.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAUtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,gCAAgC;SAA2B,OAAO;iBAAe,OAAO;mBAkGpG,CAAA"}
|