Files
clide/test/builtin/deeplink/deep_link_test.dart
T
jpmschweitzerandClaude Opus 4.8 7171a09fc2 gate clide:// deep links: paranoid allowlist + confirmation prompt (T-56, D-90)
A clide:// link is an untrusted external vector (any webpage can fire one), so it
no longer translates to a command in parseArgv. It routes the raw URL to a new
builtin.deeplink handler that is doubly defensive: a default-deny allowlist
(kDeepLinkSafeActions — only the read-only 'open' verb; run/git/write/passthrough
rejected) AND a mandatory 'an external link wants to: … allow?' confirmation
before anything runs. Records the security boundary as D-90.

The earlier silent editor.open passthrough is replaced; open still works, now
behind the prompt. Tests cover the allowlist (the boundary) + the gating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:57:43 +02:00

62 lines
2.3 KiB
Dart

/// Tests for the clide:// deep-link parser + paranoid allowlist (T-56, D-90).
/// The allowlist is the security boundary, so it gets the bulk of the coverage.
library;
import 'package:clide/builtin/deeplink/src/deep_link.dart';
import 'package:test/test.dart';
void main() {
group('parseDeepLink — open', () {
test('parses path + optional line', () {
final a = parseDeepLink('clide://open?path=/repo/x.dart')!;
expect(a.name, 'open');
expect(a.path, '/repo/x.dart');
expect(a.line, isNull);
final b = parseDeepLink('clide://open?path=/repo/x.dart&line=42')!;
expect(b.line, 42);
});
test('decodes a percent-encoded path', () {
expect(parseDeepLink('clide://open?path=/a%20b/c.dart')!.path, '/a b/c.dart');
});
test('describe reads naturally for the prompt', () {
expect(parseDeepLink('clide://open?path=/x&line=9')!.describe, contains('/x'));
expect(parseDeepLink('clide://open?path=/x&line=9')!.describe, contains('9'));
});
test('missing/empty path is rejected', () {
expect(parseDeepLink('clide://open'), isNull);
expect(parseDeepLink('clide://open?path='), isNull);
});
test('a non-positive or non-numeric line is rejected (no guessing)', () {
expect(parseDeepLink('clide://open?path=/x&line=0'), isNull);
expect(parseDeepLink('clide://open?path=/x&line=-3'), isNull);
expect(parseDeepLink('clide://open?path=/x&line=abc'), isNull);
});
});
group('paranoid allowlist (default-deny) — the security boundary', () {
test('the allowlist is exactly the safe navigation set', () {
expect(kDeepLinkSafeActions, {'open'});
});
test('a non-allowlisted action is rejected even if well-formed', () {
// These are the kind of things a malicious page might try.
expect(parseDeepLink('clide://run?cmd=rm'), isNull);
expect(parseDeepLink('clide://git?verb=push'), isNull);
expect(parseDeepLink('clide://write?path=/x&content=evil'), isNull);
expect(parseDeepLink('clide://exec?path=/x'), isNull);
});
test('a non-clide scheme or garbage is rejected', () {
expect(parseDeepLink('https://evil.com/open?path=/x'), isNull);
expect(parseDeepLink('file:///etc/passwd'), isNull);
expect(parseDeepLink('not a url at all'), isNull);
expect(parseDeepLink(''), isNull);
});
});
}