Agent Wranglers

DSK-30Required

Treat every link to the app's own scheme as untrusted input

Parse each link to the app's custom scheme, such as ledgerly://, against a fixed list of actions and argument shapes. Ask the user before an action that changes data, and never pass link text to a shell, a file path or a command-line flag.

How to do it

Any web page or email can open such a link, so its sender is a stranger. On Windows the link arrives as a command-line argument. Microsoft warns that extra quotes or backslashes in it can add arguments of the attacker's choice. Quote "%1" in the registration, and refuse a launch that holds any argument besides the one link. Electron delivers the link through open-url on macOS and second-instance on Windows and Linux, so handle both with one parser. Cap the length and test with long links and odd characters, as Microsoft advises. Hand any web address inside the link to Open outside links only after checking the address. Check each action as Check the sender and content of every message to the main process does.

Examples

JavaScript example

const MAX_LINK_LENGTH = 2048;
const ARGUMENT_SHAPES = new Map([['open', /^[a-z0-9-]{1,64}$/]]);

export function parseAppLink(raw) {
  if (typeof raw !== 'string' || raw.length > MAX_LINK_LENGTH || !URL.canParse(raw)) return null;
  const url = new URL(raw);
  const shape = ARGUMENT_SHAPES.get(url.hostname);
  const doc = url.searchParams.get('doc') ?? '';
  return url.protocol === 'ledgerly:' && shape?.test(doc) ? { action: url.hostname, doc } : null;
}

app.on('open-url', (event, url) => { event.preventDefault(); handleLink(parseAppLink(url)); });
app.on('second-instance', (_, commandLine) => handleLink(parseAppLink(commandLine.at(-1))));

Plain text example

const int MaxLinkLength = 2048;
static readonly Regex DocShape = new("^[a-z0-9-]{1,64}$");

static AppLink? ParseAppLink(string[] args)
{
    if (args.Length != 1 || args[0].Length > MaxLinkLength) return null;
    if (!Uri.TryCreate(args[0], UriKind.Absolute, out var uri) || uri.Scheme != "ledgerly") return null;
    var doc = HttpUtility.ParseQueryString(uri.Query)["doc"] ?? "";
    return uri.Host == "open" && DocShape.IsMatch(doc) ? new AppLink("open", doc) : null;
}

How to check

A test opens the app with ledgerly://open?doc=../../etc/passwd, with ledgerly://x" --inspect=9229, with a 100,000 character link and with an unknown action. Each is refused with no side effect, and no debug port opens. A valid link that deletes data shows a confirmation first.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/app-scheme-links">Agent Wranglers desktop apps and extensions playbook</a>. In code you write, add a comment next to it with the same link.