scrypted-kasa 1.2.2 → 1.2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/camera/rtsp.ts +38 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrypted-kasa",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "TP-Link Kasa plugin for Scrypted — cameras (video, two-way audio, spotlight, siren, status LED), plugs/outlets, wall switches, dimmers, and smart bulbs (on/off, brightness, color, color temperature).",
5
5
  "author": "Sergey Vilgelm <sergey@vilgelm.com>",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@ export interface RtspHandlerOptions {
27
27
  interface RtspRequest {
28
28
  method: string;
29
29
  uri: string;
30
- headers: Record<string, string>;
30
+ headers: Map<string, string>;
31
31
  }
32
32
 
33
33
  // Minimal RTSP server-side handler for ONE TCP client. Supports the methods Scrypted (and
@@ -85,7 +85,7 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
85
85
  };
86
86
 
87
87
  const handleRequest = (req: RtspRequest) => {
88
- const cseq = req.headers['cseq'] || '0';
88
+ const cseq = req.headers.get('cseq') || '0';
89
89
  switch (req.method) {
90
90
  case 'OPTIONS':
91
91
  respond(cseq, 200, 'OK', {
@@ -98,7 +98,7 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
98
98
  return;
99
99
 
100
100
  case 'SETUP': {
101
- const transport = req.headers['transport'] || '';
101
+ const transport = req.headers.get('transport') || '';
102
102
  const m = /interleaved=(\d+)-(\d+)/i.exec(transport);
103
103
  if (!m) {
104
104
  // We only support TCP-interleaved transport. UDP would require allocating
@@ -153,6 +153,12 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
153
153
  };
154
154
 
155
155
  const HEADER_TERMINATOR = Buffer.from('\r\n\r\n');
156
+ // Caps on accumulated buffer size. Server is localhost-only, but a misbehaving or
157
+ // malicious client could trickle bytes without ever sending the header terminator,
158
+ // or advertise a giant Content-Length, forcing `buffered` to grow unbounded via
159
+ // Buffer.concat. Real RTSP requests for the methods we accept are well under 1 KB.
160
+ const RTSP_MAX_HEADER_BYTES = 8 * 1024;
161
+ const RTSP_MAX_BODY_BYTES = 4 * 1024;
156
162
  client.on('data', (chunk: Buffer) => {
157
163
  buffered = buffered.length === 0 ? chunk : Buffer.concat([buffered, chunk]);
158
164
  // Parse messages until we run out of complete ones in the buffer.
@@ -168,21 +174,44 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
168
174
  continue;
169
175
  }
170
176
  const headerEndIdx = buffered.indexOf(HEADER_TERMINATOR);
177
+ // Apply the cap whether or not the terminator has arrived: a single oversized
178
+ // chunk that contains the terminator would otherwise slip past a check that
179
+ // only fires while we're still waiting for it.
180
+ const headerLen = headerEndIdx < 0 ? buffered.length : headerEndIdx;
181
+ if (headerLen > RTSP_MAX_HEADER_BYTES) {
182
+ opts.console.warn('rtsp: header section exceeded cap, dropping client');
183
+ client.destroy();
184
+ return;
185
+ }
171
186
  if (headerEndIdx < 0) return;
172
187
  const headerStr = buffered.subarray(0, headerEndIdx).toString('utf8');
173
188
  const headerLines = headerStr.split('\r\n');
174
189
  const requestLine = headerLines.shift() || '';
175
190
  const [method, uri] = requestLine.split(' ');
176
- // Null-prototype map so a malicious peer can't reach Object.prototype via a
177
- // crafted header name like `__proto__`. Practical risk is tiny — the server
178
- // only listens on localhost — but the fix is one line and shuts up CodeQL.
179
- const headers = Object.create(null) as Record<string, string>;
191
+ // Map (not a plain object) so a malicious peer can't reach Object.prototype via
192
+ // a crafted header name like `__proto__`. Practical risk is tiny — the server
193
+ // only listens on localhost — but Map is the pattern CodeQL accepts.
194
+ const headers = new Map<string, string>();
180
195
  for (const line of headerLines) {
181
196
  const colon = line.indexOf(':');
182
197
  if (colon < 0) continue;
183
- headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
198
+ headers.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
199
+ }
200
+ // Strict validation: parseInt('1abc', 10) === 1, which would let us buffer
201
+ // 1 body byte and then desync the parser on the rest. Require a pure decimal
202
+ // string before trusting the length.
203
+ const rawContentLength = headers.get('content-length');
204
+ if (rawContentLength !== undefined && !/^\d+$/.test(rawContentLength)) {
205
+ opts.console.warn('rtsp: invalid content-length', rawContentLength);
206
+ client.destroy();
207
+ return;
208
+ }
209
+ const contentLength = rawContentLength ? parseInt(rawContentLength, 10) : 0;
210
+ if (contentLength > RTSP_MAX_BODY_BYTES) {
211
+ opts.console.warn('rtsp: oversized content-length', rawContentLength);
212
+ client.destroy();
213
+ return;
184
214
  }
185
- const contentLength = parseInt(headers['content-length'] || '0', 10);
186
215
  const totalLen = headerEndIdx + HEADER_TERMINATOR.length + contentLength;
187
216
  if (buffered.length < totalLen) return;
188
217
  // We currently ignore request bodies — none of the methods we handle need one.