@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
1 lines • 16.1 kB
Source Map (JSON)
{"version":3,"file":"redact.cjs","sources":["../../../../src/core/http/redact.ts"],"sourcesContent":["/**\n * Bounded-depth redact for params that may contain credentials before they\n * enter any logger or error-rendering surface.\n *\n * Callers: `Http._sanitizeParams` (logger context), `_makeAxiosRequest`\n * (`post/send`, `post/response` and `post/catchError` info logs), `AjaxError`\n * constructor (stores `requestInfo.params` exposed by `toJSON()` /\n * `toString()`). Keeping a single source of truth means the redaction list\n * stays consistent across all of them.\n *\n * **This runs over response bodies as well as request params.** The\n * `post/response` callsite passes `response.data.result` through here, so\n * whatever the portal put in an answer is in scope too — a wider remit than\n * \"the parameters we sent\", and the reason pass 3 exists.\n *\n * Three complementary passes run over each value:\n * 1. Key match — a property whose (lower-cased) name is in\n * {@link SENSITIVE_PARAM_KEYS} has its whole value replaced, so a nested\n * credential object (e.g. `auth: { application_token }`) is masked\n * wholesale (#151).\n * 2. Query-string scrub — a *string* value is scanned for\n * `<sensitive-key>=<value>` pairs and the value is masked. This catches the\n * batch `cmd[i]` shape (`method?auth=<token>&...`) where `_prepareParams`\n * has already serialised the credential into text the key walk can't see\n * (#229).\n * 3. Credential-in-path scrub — a *string* value is scanned for the Bitrix24\n * incoming-webhook URL shape, `/rest/<userId>/<secret>/`, and the secret\n * segment is masked. Neither of the passes above can see it: the secret is\n * not a key, and it is not a `key=value` pair — it is a path segment, so\n * neither pass above can see it. It turns up in a log because this runs\n * over response bodies as well as over request params, and a portal method\n * can return such a URL: `rest.deferredbatch.downloadresult` answers\n * `{ result: { downloadUrl } }` built from the calling webhook, measured on\n * a cloud portal.\n *\n * The object walk descends two levels into nested objects and arrays — the\n * minimum that covers batch payloads (`{ cmd: [{ method, params:\n * { ...credentials... } }, ...] }`) and flat one-level-nested payloads like\n * `{ data: { token } }`.\n *\n * Residual risk (documented, accepted):\n * - credential keys nested deeper than two object levels are NOT masked —\n * redact at the callsite for those;\n * - the query-string scrub only masks a `key=value` pair whose key is itself\n * a sensitive key; a bracketed/encoded query key (`auth[application_token]=`)\n * is not matched by the string pass (its `auth` prefix object form is,\n * though, via pass 1). This covers the v2 file links\n * (`uf.php?…&auth[ap]=…`, e.g. `task.item.getfiles` `DOWNLOAD_URL`). The\n * decision was made in PR #235 and is closed; do not reopen it.\n * - `key` is deliberately broad: any property literally named `key` (and any\n * `?key=…` query pair) is masked. In Bitrix24 REST `key` is a credential\n * parameter (e.g. the Pull shared config), so this is a conservative,\n * accepted trade-off — it can over-redact a non-credential field that\n * happens to be named `key`.\n * - `signature` is broad in the same way (added in #43 for the Pull channel\n * HMAC, `TypeChanel.signature`): any property named `signature` and any\n * `?signature=…` query pair is masked. In the Bitrix24 push/pull domain\n * `signature` is the channel HMAC, so the breadth is accepted — at the cost\n * of over-redacting a non-credential field that happens to be named so.\n * - empty / nullish values are still treated as sensitive — an empty\n * `access_token` is unusual but not safe to leave un-redacted.\n * - the path scrub matches one shape — Bitrix24's own webhook URL — and not\n * \"a credential somewhere in a path\" in general, which is not a decidable\n * question. A credential that some other service puts in a path is not\n * covered.\n */\n\nexport const SENSITIVE_PARAM_KEYS: readonly string[] = [\n 'auth',\n 'password',\n 'token',\n 'secret',\n 'access_token',\n 'refresh_token',\n 'client_secret',\n 'application_token',\n 'sessid',\n 'key',\n 'signature'\n]\n\nexport const REDACTED_PLACEHOLDER = '***REDACTED***'\n\n// Matches `<sep><sensitive-key>=<value>` inside a string, case-insensitively,\n// and masks the value. The `([?&]|^)` prefix anchors to a real query-param\n// boundary so a credential name appearing inside a value (`foo=token=x`) or as\n// the tail of a longer key (`access_token` vs `token`) is not mis-matched. The\n// value runs to the next `&`, `#`, or `;`, so a `;`-separated adjacent param is\n// not swallowed into the redacted span. An embedded `?token=…` inside a nested\n// URL value IS masked (intended — still a credential). Single-line: `^` carries\n// no `m` flag, so a credential after a newline in a multi-line string value is\n// not caught (accepted residual risk, same class as encoded/bracketed keys).\nconst QS_SENSITIVE_RE = new RegExp(\n `([?&]|^)(${SENSITIVE_PARAM_KEYS.join('|')})=[^&#;]*`,\n 'gi'\n)\n\n/**\n * The Bitrix24 incoming-webhook URL shape: `/rest/<userId>/<secret>/`.\n *\n * Deliberately narrow, because a path segment carries no name to match on and\n * the only defence against over-masking is the shape itself:\n *\n * - `<userId>` is digits, as the portal builds it;\n * - `<secret>` carries no dot, so a REST method name never matches — those do\n * (`crm.item.list`, `rest.deferredbatch.downloadresult`). Hyphen and\n * underscore are inside the class even though Bitrix24 issues alphanumeric\n * secrets: the SDK does not control that format, and guessing it too narrowly\n * fails in the direction that matters;\n * - at least 8 characters, which no short path word (`batch`, `profile`,\n * `download`) reaches, and every real webhook secret does — they are issued\n * far longer.\n *\n * **Both API versions.** `restApi:v3` puts the webhook at `/rest/api/<id>/…`\n * rather than `/rest/<id>/…` (`B24Hook.fromWebhookUrl`), so the `api/` segment\n * is optional here — a pattern written for v2 alone would leave every v3 hook\n * unmasked.\n *\n * **The segment may end the string.** `.../rest/1/<secret>` with no trailing\n * slash is a legitimate webhook URL — it is the form `fromWebhookUrl` accepts —\n * so the boundary is `/`, `?`, `#` or end of input, not `/` alone. This is\n * where the shape stops being conservative: `/rest/1/somedotlessword` at the end\n * of a string is masked too. Accepted deliberately — dotless REST method names\n * are short (`batch`, `scope`, `profile`) and fall under the length floor, and\n * masking a method name costs a line of debugging detail where missing a secret\n * costs rather more.\n *\n * Case-insensitive, matching the query-string pass. The portal issues a\n * lower-case `/rest/`, but a URL that reached a log may have been copied,\n * proxied or hand-written, and `/REST/` guards nothing by staying literal.\n *\n * Only the secret is masked; the portal host and the user id stay readable,\n * because a redacted line still has to be usable for debugging.\n */\nconst WEBHOOK_PATH_RE = /(\\/rest\\/(?:api\\/)?\\d+\\/)[\\w-]{8,}(?=[/?#]|$)/gi\n\n// Safe to share at module scope despite the `g` flag: `String.replace` scans\n// from 0 and resets `lastIndex` when it finishes, and this regex is only ever\n// used that way. A `.test()` or `.exec()` call on it would carry `lastIndex`\n// between calls and start skipping matches — do not add one.\nfunction redactWebhookPath(value: string): string {\n return value.replace(WEBHOOK_PATH_RE, `$1${REDACTED_PLACEHOLDER}`)\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction redactQueryString(value: string): string {\n if (!value.includes('=')) return value\n return value.replace(\n QS_SENSITIVE_RE,\n (_match, sep: string, key: string) => `${sep}${key}=${REDACTED_PLACEHOLDER}`\n )\n}\n\n/**\n * Both string passes, in the order that keeps each one's fast path honest.\n *\n * The query scrub returns early when the string holds no `=` at all, which is\n * correct for a query pair and wrong for a path: `.../rest/1/<secret>/download/`\n * has no `=` anywhere, so routing every string through the query pass alone\n * left exactly that URL untouched.\n */\nfunction redactString(value: string): string {\n return redactWebhookPath(redactQueryString(value))\n}\n\n// String scrubbing runs before the `depth <= 0` guard on purpose: scanning a\n// string is cheap and bounded, so a serialised credential is masked even at a\n// level the object walk would stop descending into. Arrays do not consume a\n// depth slot (only object descent decrements `depth`), so an array nested in an\n// array is still walked.\nfunction redactValue(value: unknown, depth: number): unknown {\n if (typeof value === 'string') return redactString(value)\n if (depth <= 0) return value\n if (isPlainObject(value)) return redactObject(value, depth - 1)\n if (Array.isArray(value)) return value.map(item => redactValue(item, depth))\n return value\n}\n\nfunction redactObject(\n source: Record<string, unknown>,\n depth: number\n): Record<string, unknown> {\n const sanitized: Record<string, unknown> = { ...source }\n for (const key of Object.keys(sanitized)) {\n if (SENSITIVE_PARAM_KEYS.includes(key.toLowerCase())) {\n sanitized[key] = REDACTED_PLACEHOLDER\n continue\n }\n sanitized[key] = redactValue(sanitized[key], depth)\n }\n return sanitized\n}\n\nconst DEFAULT_REDACT_DEPTH = 2\n\n/**\n * Returns a copy of `params` with any known credential-bearing key replaced by\n * `REDACTED_PLACEHOLDER`, and any credential embedded in a string — a query\n * value or a webhook secret in a URL path — masked in place. Walks up to two\n * levels into nested objects/arrays so batch-shaped payloads\n * (`cmd[i].params.<key>` and `cmd[i]` query strings) are covered.\n *\n * **An array or a string at the top level is walked too.** It used to be\n * returned untouched: this function began `if (!isPlainObject(params)) return\n * params`, and `isPlainObject` excludes arrays. The walker underneath has\n * always handled both — only the door was shut. So the same content was masked\n * inside an object and printed verbatim when it arrived on its own, and #468\n * widened that gap rather than closing it, by teaching the string pass to mask\n * a webhook secret in a URL path that a top-level string never reached.\n *\n * That is not a corner: `_makeAxiosRequest` logs `response.data?.result`, and a\n * `restApi:v3` batch answers with an array there, so every successful v3 batch\n * wrote its response to the log unmasked.\n *\n * The plain-object branch is kept rather than folded into `redactValue`,\n * because entering through the value walker would spend a depth level on the\n * object itself and leave one for its contents — half the reach that\n * batch-shaped payloads need.\n */\nexport function redactSensitiveParams(\n params: Record<string, unknown>\n): Record<string, unknown>\nexport function redactSensitiveParams<T>(params: T): T\nexport function redactSensitiveParams(params: unknown): unknown {\n if (isPlainObject(params)) return redactObject(params, DEFAULT_REDACT_DEPTH)\n return redactValue(params, DEFAULT_REDACT_DEPTH)\n}\n\n/**\n * Redact credentials in a URL string — e.g. a Pull `connectionPath` surfaced by\n * `getDebugInfo()`. Masks the webhook secret when the URL carries one in its\n * path (`/rest/<userId>/<secret>/`), and every\n * {@link SENSITIVE_PARAM_KEYS} value plus any caller-supplied `extraKeys`\n * (e.g. Pull's `CHANNEL_ID`, a private identifier that is not a global\n * credential key). `extraKeys` are regex-escaped, so any literal key name is\n * safe to pass. Anchored and bounded exactly like the in-object scrub, so a\n * value-position `=` and non-query strings are left intact. Non-string input is\n * returned unchanged (a defensive guard for untyped JS callers).\n */\nexport function redactSensitiveUrl(url: string, extraKeys: readonly string[] = []): string {\n if (typeof url !== 'string') return url\n // The webhook-path pass runs first and unconditionally: a webhook URL with no\n // query string at all holds no `=`, and the query scrub's early return would\n // otherwise hand it back with the secret intact.\n const masked = redactWebhookPath(url)\n if (!masked.includes('=')) return masked\n if (extraKeys.length === 0) return redactQueryString(masked)\n const escaped = extraKeys.map(key => key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n const re = new RegExp(\n `([?&]|^)(${[...SENSITIVE_PARAM_KEYS, ...escaped].join('|')})=[^&#;]*`,\n 'gi'\n )\n return masked.replace(re, (_match, sep: string, key: string) => `${sep}${key}=${REDACTED_PLACEHOLDER}`)\n}\n"],"names":[],"mappings":"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"}