-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsanitize-blocks.ts
More file actions
118 lines (113 loc) · 4.29 KB
/
Copy pathsanitize-blocks.ts
File metadata and controls
118 lines (113 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import type { SupportedBlock } from '../types';
import { sanitizeHref, sanitizeImageSrc } from './url-safety';
/**
* Recursively walks an unknown value and rewrites string values at the
* specified keys via the matching sanitizer. Used to strip dangerous
* URI schemes (e.g. `javascript:`) from `url`, `image_url`, and
* rich-text link `url` fields before a Block Kit payload reaches a
* renderer or is handed back to the consumer.
*
* Allocates a new object whenever a child is rewritten; otherwise
* returns the input unchanged so unaffected payloads are reference-stable.
*
* Defensive against prototype-polluted shapes: copies are made via
* `Object.assign({}, ...)` over own enumerable keys returned by
* `Object.keys`, which skips inherited properties.
*/
const HREF_KEYS = new Set(['url']);
const IMAGE_KEYS = new Set(['image_url']);
/**
* Read-only metadata Slack attaches when a message is *retrieved* via the
* API, but rejects on *send*, keyed by the `type` of the object it appears
* on. Blocks loaded from an existing message carry these; drop them only
* from the matching object type — some are common field names (`fallback`)
* that are valid elsewhere — so a round-tripped payload stays send-valid
* without over-scrubbing. Add an entry to extend this to other block types.
*/
const RETRIEVAL_ONLY_KEYS = new Map<string, Set<string>>([
// Covers both the image block and the image element — both `type: 'image'`.
['image', new Set(['image_width', 'image_height', 'image_bytes', 'fallback', 'is_animated'])],
// Slack renders the chart server-side and attaches the rendered previews on
// retrieval; sending them back is rejected as `unknown property 'preview_images'`.
['data_visualization', new Set(['preview_images'])]
]);
/**
* Recursively sanitize all known URL-bearing string fields inside a
* Slack Block Kit payload fragment. Returns a value with the same
* structural shape, where any field whose name matches an href or
* image-src key has been replaced by the safe variant (`''` if the
* original scheme was unsafe).
* @param value - any payload fragment (object, array, primitive)
* @returns the sanitized payload fragment
*/
function sanitizeValue(value: unknown): unknown {
if (value === null || typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
let changed = false;
const out = new Array(value.length);
for (let i = 0; i < value.length; i++) {
const next = sanitizeValue(value[i]);
if (next !== value[i]) {
changed = true;
}
out[i] = next;
}
return changed ? out : value;
}
const src = value as Record<string, unknown>;
const dropKeys = typeof src.type === 'string' ? RETRIEVAL_ONLY_KEYS.get(src.type) : undefined;
let copy: Record<string, unknown> | null = null;
for (const key of Object.keys(src)) {
if (dropKeys?.has(key)) {
copy ??= { ...src };
delete copy[key];
continue;
}
const original = src[key];
let next: unknown = original;
if (typeof original === 'string') {
if (HREF_KEYS.has(key)) {
next = sanitizeHref(original);
} else if (IMAGE_KEYS.has(key)) {
next = sanitizeImageSrc(original);
}
} else if (typeof original === 'object' && original !== null) {
next = sanitizeValue(original);
}
if (next !== original) {
if (!copy) {
copy = { ...src };
}
copy[key] = next;
}
}
return copy ?? src;
}
/**
* Sanitize a single Block Kit block, scrubbing dangerous URI schemes
* from `url` and `image_url` fields anywhere in the payload tree.
* @param block - the block payload to sanitize
* @returns the sanitized block (same reference if nothing changed)
*/
export function sanitizeBlock<T extends SupportedBlock>(block: T): T {
return sanitizeValue(block) as T;
}
/**
* Sanitize an array of Block Kit blocks. See {@link sanitizeBlock}.
* @param blocks - the block payloads to sanitize
* @returns the sanitized blocks
*/
export function sanitizeBlocks(blocks: SupportedBlock[]): SupportedBlock[] {
let changed = false;
const out = new Array<SupportedBlock>(blocks.length);
for (let i = 0; i < blocks.length; i++) {
const next = sanitizeBlock(blocks[i]);
if (next !== blocks[i]) {
changed = true;
}
out[i] = next;
}
return changed ? out : blocks;
}