@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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
- <div
82
- key={link.id}
83
- className="flex items-center justify-between p-3 gap-2"
84
- >
85
- <div className="flex items-center gap-2 min-w-0 flex-1">
86
- <ExternalLink className="h-4 w-4 text-muted-foreground shrink-0" />
87
- <div className="min-w-0">
88
- <a
89
- href={link.url}
90
- target="_blank"
91
- rel="noopener noreferrer"
92
- className="text-sm text-primary hover:underline truncate block"
93
- >
94
- {link.label ?? link.url}
95
- </a>
96
- {link.label && (
97
- <span className="text-xs text-muted-foreground truncate block">
98
- {link.url}
99
- </span>
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
- {canManage && (
104
- <Button
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>