improve url validation for auth47 and lnurl-auth

This commit is contained in:
Craig Raw
2026-05-30 11:34:53 +02:00
parent 79517da131
commit 464fade68f
5 changed files with 112 additions and 10 deletions
+1 -1
Submodule drongo updated: e96aa0d3f8...7d0699faa0
@@ -4,6 +4,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
@@ -69,19 +70,17 @@ public class Auth47 {
this.srbnName = srbnUrl.getUserInfo();
this.callback = new URI(HTTPS_PROTOCOL + srbnUrl.getHost()).toURL();
} else {
this.callback = new URI(strCallback).toURL();
URI callbackUri = new URI(strCallback);
if(!Utils.isSecureUrl(callbackUri)) {
throw new IllegalArgumentException("Invalid callback parameter (not https, http .onion or srbn): " + strCallback);
}
this.callback = callbackUri.toURL();
}
this.expiry = parameterMap.get("e");
this.resource = parameterMap.get("r");
if(resource == null) {
if(srbn) {
this.resource = "srbn";
} else if(strCallback.startsWith("http")) {
this.resource = strCallback;
} else {
throw new IllegalArgumentException("Invalid callback parameter (not http/s or srbn): " + strCallback);
}
this.resource = srbn ? "srbn" : strCallback;
}
}
@@ -41,9 +41,17 @@ public class LnurlAuth {
public LnurlAuth(URI uri) throws MalformedURLException, URISyntaxException {
String lnurl = uri.getSchemeSpecificPart();
Bech32.Bech32Data bech32 = Bech32.decode(lnurl, 2000);
if(!"lnurl".equals(bech32.hrp)) {
throw new IllegalArgumentException("LNURL-auth bech32 prefix must be lnurl");
}
byte[] urlBytes = Bech32.convertBits(bech32.data, 0, bech32.data.length, 5, 8, false);
String strUrl = new String(urlBytes, StandardCharsets.UTF_8);
this.url = new URI(strUrl).toURL();
URI decodedUri = new URI(strUrl);
if(!Utils.isSecureUrl(decodedUri)) {
throw new IllegalArgumentException("LNURL-auth URL must be https or http .onion");
}
this.url = decodedUri.toURL();
Map<String, String> parameterMap = new LinkedHashMap<>();
String query = url.getQuery();
@@ -0,0 +1,38 @@
package com.sparrowwallet.sparrow.net;
import org.junit.jupiter.api.Test;
import java.net.URI;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class Auth47Test {
@Test
public void acceptsHttpsCallbacksWithResource() throws Exception {
Auth47 auth47 = new Auth47(new URI("auth47://nonce?c=https://example.com/auth&r=example"));
assertEquals("https", auth47.getCallback().getProtocol());
assertEquals("example.com", auth47.getCallback().getHost());
}
@Test
public void acceptsSrbnCallbacks() throws Exception {
Auth47 auth47 = new Auth47(new URI("auth47://nonce?c=srbn://alice@relay.example.com"));
assertEquals("https", auth47.getCallback().getProtocol());
assertEquals("relay.example.com", auth47.getCallback().getHost());
}
@Test
public void rejectsHttpClearnetCallbacks() {
assertThrows(IllegalArgumentException.class, () ->
new Auth47(new URI("auth47://nonce?c=http://example.com/auth&r=example")));
}
@Test
public void rejectsNonHttpCallbacksWithResource() {
assertThrows(IllegalArgumentException.class, () ->
new Auth47(new URI("auth47://nonce?c=file:///tmp/auth47&r=example")));
}
}
@@ -0,0 +1,57 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.drongo.protocol.Bech32;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class LnurlAuthTest {
private static final String K1 = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
@Test
public void acceptsHttpsCallbacks() throws Exception {
LnurlAuth lnurlAuth = new LnurlAuth(lightningUri("https://example.com/lnurl-auth?tag=login&k1=" + K1));
assertEquals("example.com", lnurlAuth.getDomain());
assertEquals("login to example.com", lnurlAuth.getLoginMessage());
}
@Test
public void acceptsHttpOnionCallbacks() throws Exception {
LnurlAuth lnurlAuth = new LnurlAuth(lightningUri("http://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/lnurl-auth?tag=login&k1=" + K1));
assertEquals("abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion", lnurlAuth.getDomain());
}
@Test
public void rejectsHttpClearnetCallbacks() {
assertThrows(IllegalArgumentException.class, () ->
new LnurlAuth(lightningUri("http://example.com/lnurl-auth?tag=login&k1=" + K1)));
}
@Test
public void rejectsNonHttpCallbacks() {
assertThrows(IllegalArgumentException.class, () ->
new LnurlAuth(lightningUri("ftp://example.com/lnurl-auth?tag=login&k1=" + K1)));
}
@Test
public void rejectsNonLnurlBech32Prefix() {
assertThrows(IllegalArgumentException.class, () ->
new LnurlAuth(lightningUri("lnurlx", "https://example.com/lnurl-auth?tag=login&k1=" + K1)));
}
private static URI lightningUri(String url) throws Exception {
return lightningUri("lnurl", url);
}
private static URI lightningUri(String prefix, String url) throws Exception {
byte[] urlBytes = url.getBytes(StandardCharsets.UTF_8);
byte[] lnurlData = Bech32.convertBits(urlBytes, 0, urlBytes.length, 8, 5, true);
return new URI("lightning:" + Bech32.encode(prefix, Bech32.Encoding.BECH32, lnurlData));
}
}