@checkstack/ui 1.8.2 → 1.8.3
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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/components/LinksEditor.tsx +69 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 1.8.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1909a61: Address open CodeQL code-scanning findings:
|
|
8
|
+
|
|
9
|
+
- **`@checkstack/ui` (`LinksEditor`)**: validate URL scheme on render and on
|
|
10
|
+
add; only `http:` / `https:` URLs are accepted, defeating stored XSS via
|
|
11
|
+
`javascript:` / `data:` schemes in user-supplied hotlinks
|
|
12
|
+
(`js/xss-through-dom`).
|
|
13
|
+
- **`@checkstack/backend-api` (`markdownToPlainText`)**: decode HTML entities
|
|
14
|
+
before stripping tags, then strip tags in a loop until the output
|
|
15
|
+
stabilizes. Decoding `&` last avoids reintroducing tag delimiters
|
|
16
|
+
via `<` round-trips (`js/double-escaping`,
|
|
17
|
+
`js/incomplete-multi-character-sanitization`).
|
|
18
|
+
- **`@checkstack/backend` (`createScopedWsRegistry`)**: drop the
|
|
19
|
+
identity-replacement on the path suffix; the leading-slash invariant
|
|
20
|
+
is documented on `WebSocketRouteRegistry` (`js/identity-replacement`).
|
|
21
|
+
|
|
3
22
|
## 1.8.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -10,6 +10,24 @@ export interface HotLink {
|
|
|
10
10
|
url: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Parse a user-supplied URL and return its canonicalized form ONLY when the
|
|
14
|
+
// scheme is `http:` / `https:`. Returns `undefined` for any other scheme
|
|
15
|
+
// (`javascript:`, `data:`, `vbscript:`, etc.) so callers can refuse to
|
|
16
|
+
// render an anchor at all. The returned string is `URL.toString()` — i.e.
|
|
17
|
+
// a re-serialized URL — so taint analysis sees a fresh, sanitized value
|
|
18
|
+
// rather than the raw input flowing through. CodeQL js/xss-through-dom.
|
|
19
|
+
function safeHref(raw: string): string | undefined {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(raw);
|
|
22
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
23
|
+
return parsed.toString();
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
export interface LinksEditorProps<T extends HotLink> {
|
|
14
32
|
/** Currently attached links. */
|
|
15
33
|
links: T[];
|
|
@@ -60,6 +78,11 @@ export function LinksEditor<T extends HotLink>({
|
|
|
60
78
|
setError("Must be a valid URL (include http:// or https://)");
|
|
61
79
|
return;
|
|
62
80
|
}
|
|
81
|
+
const parsed = new URL(trimmedUrl);
|
|
82
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
83
|
+
setError("Only http:// and https:// URLs are allowed");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
63
86
|
setError(undefined);
|
|
64
87
|
await onAdd({ label: label.trim() || undefined, url: trimmedUrl });
|
|
65
88
|
setLabel("");
|
|
@@ -77,42 +100,54 @@ export function LinksEditor<T extends HotLink>({
|
|
|
77
100
|
|
|
78
101
|
{links.length > 0 ? (
|
|
79
102
|
<div className="border rounded-lg divide-y">
|
|
80
|
-
{links.map((link) =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<div className="min-w-0">
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
{links.map((link) => {
|
|
104
|
+
const href = safeHref(link.url);
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
key={link.id}
|
|
108
|
+
className="flex items-center justify-between p-3 gap-2"
|
|
109
|
+
>
|
|
110
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
111
|
+
<ExternalLink className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
112
|
+
<div className="min-w-0">
|
|
113
|
+
{href ? (
|
|
114
|
+
<a
|
|
115
|
+
href={href}
|
|
116
|
+
target="_blank"
|
|
117
|
+
rel="noopener noreferrer"
|
|
118
|
+
className="text-sm text-primary hover:underline truncate block"
|
|
119
|
+
>
|
|
120
|
+
{link.label ?? link.url}
|
|
121
|
+
</a>
|
|
122
|
+
) : (
|
|
123
|
+
<span
|
|
124
|
+
className="text-sm text-muted-foreground truncate block"
|
|
125
|
+
title="Unsafe URL scheme — link disabled"
|
|
126
|
+
>
|
|
127
|
+
{link.label ?? link.url}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
{link.label && (
|
|
131
|
+
<span className="text-xs text-muted-foreground truncate block">
|
|
132
|
+
{link.url}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
101
136
|
</div>
|
|
137
|
+
{canManage && (
|
|
138
|
+
<Button
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="sm"
|
|
141
|
+
onClick={() => void onRemove(link)}
|
|
142
|
+
disabled={busy}
|
|
143
|
+
aria-label="Remove link"
|
|
144
|
+
>
|
|
145
|
+
<Trash2 className="h-4 w-4" />
|
|
146
|
+
</Button>
|
|
147
|
+
)}
|
|
102
148
|
</div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
variant="ghost"
|
|
106
|
-
size="sm"
|
|
107
|
-
onClick={() => void onRemove(link)}
|
|
108
|
-
disabled={busy}
|
|
109
|
-
aria-label="Remove link"
|
|
110
|
-
>
|
|
111
|
-
<Trash2 className="h-4 w-4" />
|
|
112
|
-
</Button>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
))}
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
116
151
|
</div>
|
|
117
152
|
) : (
|
|
118
153
|
<p className="text-sm text-muted-foreground">No links attached</p>
|