@stellar/typescript-wallet-sdk 3.0.2 → 3.0.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.
- package/CHANGELOG.MD +8 -0
- package/lib/bundle.js +42 -2
- package/lib/bundle.js.map +1 -1
- package/lib/bundle_browser.js +42 -2
- package/lib/bundle_browser.js.map +1 -1
- package/lib/walletSdk/Types/sep7.d.ts +18 -0
- package/package.json +1 -1
- package/src/walletSdk/Types/sep7.ts +22 -0
- package/src/walletSdk/Uri/Sep7Base.ts +14 -1
- package/src/walletSdk/Uri/sep7Parser.ts +14 -0
- package/test/sep7.test.ts +138 -1
|
@@ -14,3 +14,21 @@ export type IsValidSep7UriResult = {
|
|
|
14
14
|
result: boolean;
|
|
15
15
|
reason?: string;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Matches a fully qualified domain name (e.g. "example.com", "sub.example.co.uk").
|
|
19
|
+
*
|
|
20
|
+
* Rejects userinfo ('@'), ports (':'), paths/queries ('/', '?'), scheme
|
|
21
|
+
* prefixes ('//'), bare hostnames like 'localhost', and raw IPv4/IPv6
|
|
22
|
+
* addresses (the all-numeric final label fails the alphabetic TLD rule).
|
|
23
|
+
*
|
|
24
|
+
* @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md#verifying-the-signature step 3.
|
|
25
|
+
*/
|
|
26
|
+
export declare const FULLY_QUALIFIED_DOMAIN_NAME_REGEX: RegExp;
|
|
27
|
+
/**
|
|
28
|
+
* Returns true if the given string is a valid fully qualified domain name.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} domain the domain string to validate.
|
|
31
|
+
*
|
|
32
|
+
* @returns {boolean} true if 'domain' is a valid fully qualified domain name.
|
|
33
|
+
*/
|
|
34
|
+
export declare const isValidFullyQualifiedDomainName: (domain: string) => boolean;
|
package/package.json
CHANGED
|
@@ -18,3 +18,25 @@ export type IsValidSep7UriResult = {
|
|
|
18
18
|
result: boolean;
|
|
19
19
|
reason?: string;
|
|
20
20
|
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Matches a fully qualified domain name (e.g. "example.com", "sub.example.co.uk").
|
|
24
|
+
*
|
|
25
|
+
* Rejects userinfo ('@'), ports (':'), paths/queries ('/', '?'), scheme
|
|
26
|
+
* prefixes ('//'), bare hostnames like 'localhost', and raw IPv4/IPv6
|
|
27
|
+
* addresses (the all-numeric final label fails the alphabetic TLD rule).
|
|
28
|
+
*
|
|
29
|
+
* @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md#verifying-the-signature step 3.
|
|
30
|
+
*/
|
|
31
|
+
export const FULLY_QUALIFIED_DOMAIN_NAME_REGEX =
|
|
32
|
+
/^(?=.{1,253}$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true if the given string is a valid fully qualified domain name.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} domain the domain string to validate.
|
|
38
|
+
*
|
|
39
|
+
* @returns {boolean} true if 'domain' is a valid fully qualified domain name.
|
|
40
|
+
*/
|
|
41
|
+
export const isValidFullyQualifiedDomainName = (domain: string): boolean =>
|
|
42
|
+
FULLY_QUALIFIED_DOMAIN_NAME_REGEX.test(domain);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Keypair, Networks, StellarToml } from "@stellar/stellar-sdk";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Sep7OperationType,
|
|
4
|
+
URI_MSG_MAX_LENGTH,
|
|
5
|
+
isValidFullyQualifiedDomainName,
|
|
6
|
+
} from "../Types";
|
|
3
7
|
import { Sep7LongMsgError } from "../Exceptions";
|
|
4
8
|
|
|
5
9
|
/**
|
|
@@ -234,6 +238,15 @@ export abstract class Sep7Base {
|
|
|
234
238
|
return false;
|
|
235
239
|
}
|
|
236
240
|
|
|
241
|
+
// SEP-7 step 3: "If the origin_domain is not a valid fully qualified
|
|
242
|
+
// domain name, do not allow the user to sign the transaction." This must
|
|
243
|
+
// be enforced before resolving the stellar.toml, otherwise a malformed
|
|
244
|
+
// origin_domain (e.g. containing userinfo, a port, or a path) could
|
|
245
|
+
// redirect the trust-anchoring TOML fetch to an attacker-controlled host.
|
|
246
|
+
if (!isValidFullyQualifiedDomainName(originDomain)) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
237
250
|
try {
|
|
238
251
|
const toml = await StellarToml.Resolver.resolve(originDomain);
|
|
239
252
|
const signingKey = toml.URI_REQUEST_SIGNING_KEY;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
WEB_STELLAR_SCHEME,
|
|
9
9
|
URI_MSG_MAX_LENGTH,
|
|
10
10
|
URI_REPLACE_MAX_LENGTH,
|
|
11
|
+
isValidFullyQualifiedDomainName,
|
|
11
12
|
} from "../Types";
|
|
12
13
|
import {
|
|
13
14
|
Sep7InvalidUriError,
|
|
@@ -42,6 +43,7 @@ export const isValidSep7Uri = (uri: string): IsValidSep7UriResult => {
|
|
|
42
43
|
url.searchParams.get("network_passphrase") || Networks.PUBLIC;
|
|
43
44
|
const destination = url.searchParams.get("destination");
|
|
44
45
|
const msg = url.searchParams.get("msg");
|
|
46
|
+
const originDomain = url.searchParams.get("origin_domain");
|
|
45
47
|
|
|
46
48
|
if (![Sep7OperationType.tx, Sep7OperationType.pay].includes(type)) {
|
|
47
49
|
return {
|
|
@@ -98,6 +100,18 @@ export const isValidSep7Uri = (uri: string): IsValidSep7UriResult => {
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
// 'origin_domain' is optional (SEP-7), but when present it must be a valid
|
|
104
|
+
// fully qualified domain name (SEP-7 step 3), otherwise a malformed value
|
|
105
|
+
// could later redirect the trust-anchoring stellar.toml fetch in
|
|
106
|
+
// Sep7Base.verifySignature() to an attacker-controlled host.
|
|
107
|
+
if (originDomain && !isValidFullyQualifiedDomainName(originDomain)) {
|
|
108
|
+
return {
|
|
109
|
+
result: false,
|
|
110
|
+
reason:
|
|
111
|
+
"the provided 'origin_domain' parameter is not a valid fully qualified domain name",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
return {
|
|
102
116
|
result: true,
|
|
103
117
|
};
|
package/test/sep7.test.ts
CHANGED
|
@@ -226,6 +226,115 @@ describe("Sep7Base", () => {
|
|
|
226
226
|
|
|
227
227
|
expect(await uri.verifySignature()).toBe(true);
|
|
228
228
|
});
|
|
229
|
+
|
|
230
|
+
it("verifySignature() returns false and never resolves the toml when origin_domain has userinfo (RFC 3986 confusion)", async () => {
|
|
231
|
+
const uriStr =
|
|
232
|
+
"web+stellar:tx?xdr=test&origin_domain=stellar.org%40evil.com&signature=sig";
|
|
233
|
+
const uri = new Sep7Tx(uriStr);
|
|
234
|
+
|
|
235
|
+
const resolveSpy = jest
|
|
236
|
+
.spyOn(StellarToml.Resolver, "resolve")
|
|
237
|
+
.mockResolvedValue({
|
|
238
|
+
URI_REQUEST_SIGNING_KEY: testKp1.publicKey,
|
|
239
|
+
});
|
|
240
|
+
// jest.spyOn() re-spying on an already-mocked function preserves the
|
|
241
|
+
// existing call history, so clear it to accurately assert non-invocation.
|
|
242
|
+
resolveSpy.mockClear();
|
|
243
|
+
|
|
244
|
+
expect(await uri.verifySignature()).toBe(false);
|
|
245
|
+
expect(resolveSpy).not.toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("verifySignature() returns false and never resolves the toml when origin_domain has a port", async () => {
|
|
249
|
+
const uriStr =
|
|
250
|
+
"web+stellar:tx?xdr=test&origin_domain=example.com%3A8443&signature=sig";
|
|
251
|
+
const uri = new Sep7Tx(uriStr);
|
|
252
|
+
|
|
253
|
+
const resolveSpy = jest
|
|
254
|
+
.spyOn(StellarToml.Resolver, "resolve")
|
|
255
|
+
.mockResolvedValue({
|
|
256
|
+
URI_REQUEST_SIGNING_KEY: testKp1.publicKey,
|
|
257
|
+
});
|
|
258
|
+
// jest.spyOn() re-spying on an already-mocked function preserves the
|
|
259
|
+
// existing call history, so clear it to accurately assert non-invocation.
|
|
260
|
+
resolveSpy.mockClear();
|
|
261
|
+
|
|
262
|
+
expect(await uri.verifySignature()).toBe(false);
|
|
263
|
+
expect(resolveSpy).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("verifySignature() returns false and never resolves the toml when origin_domain has a path", async () => {
|
|
267
|
+
const uriStr =
|
|
268
|
+
"web+stellar:tx?xdr=test&origin_domain=example.com%2Fpath&signature=sig";
|
|
269
|
+
const uri = new Sep7Tx(uriStr);
|
|
270
|
+
|
|
271
|
+
const resolveSpy = jest
|
|
272
|
+
.spyOn(StellarToml.Resolver, "resolve")
|
|
273
|
+
.mockResolvedValue({
|
|
274
|
+
URI_REQUEST_SIGNING_KEY: testKp1.publicKey,
|
|
275
|
+
});
|
|
276
|
+
// jest.spyOn() re-spying on an already-mocked function preserves the
|
|
277
|
+
// existing call history, so clear it to accurately assert non-invocation.
|
|
278
|
+
resolveSpy.mockClear();
|
|
279
|
+
|
|
280
|
+
expect(await uri.verifySignature()).toBe(false);
|
|
281
|
+
expect(resolveSpy).not.toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("verifySignature() returns false and never resolves the toml when origin_domain is a raw IPv4 address", async () => {
|
|
285
|
+
const uriStr =
|
|
286
|
+
"web+stellar:tx?xdr=test&origin_domain=127.0.0.1&signature=sig";
|
|
287
|
+
const uri = new Sep7Tx(uriStr);
|
|
288
|
+
|
|
289
|
+
const resolveSpy = jest
|
|
290
|
+
.spyOn(StellarToml.Resolver, "resolve")
|
|
291
|
+
.mockResolvedValue({
|
|
292
|
+
URI_REQUEST_SIGNING_KEY: testKp1.publicKey,
|
|
293
|
+
});
|
|
294
|
+
// jest.spyOn() re-spying on an already-mocked function preserves the
|
|
295
|
+
// existing call history, so clear it to accurately assert non-invocation.
|
|
296
|
+
resolveSpy.mockClear();
|
|
297
|
+
|
|
298
|
+
expect(await uri.verifySignature()).toBe(false);
|
|
299
|
+
expect(resolveSpy).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("verifySignature() returns false and never resolves the toml when origin_domain is 'localhost'", async () => {
|
|
303
|
+
const uriStr =
|
|
304
|
+
"web+stellar:tx?xdr=test&origin_domain=localhost&signature=sig";
|
|
305
|
+
const uri = new Sep7Tx(uriStr);
|
|
306
|
+
|
|
307
|
+
const resolveSpy = jest
|
|
308
|
+
.spyOn(StellarToml.Resolver, "resolve")
|
|
309
|
+
.mockResolvedValue({
|
|
310
|
+
URI_REQUEST_SIGNING_KEY: testKp1.publicKey,
|
|
311
|
+
});
|
|
312
|
+
// jest.spyOn() re-spying on an already-mocked function preserves the
|
|
313
|
+
// existing call history, so clear it to accurately assert non-invocation.
|
|
314
|
+
resolveSpy.mockClear();
|
|
315
|
+
|
|
316
|
+
expect(await uri.verifySignature()).toBe(false);
|
|
317
|
+
expect(resolveSpy).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("verifySignature() returns false and never resolves the toml when origin_domain has an injected CRLF", async () => {
|
|
321
|
+
// 'stellar.org%0d%0aevil.com' URL-decodes to 'stellar.org\r\nevil.com'.
|
|
322
|
+
const uriStr =
|
|
323
|
+
"web+stellar:tx?xdr=test&origin_domain=stellar.org%0d%0aevil.com&signature=sig";
|
|
324
|
+
const uri = new Sep7Tx(uriStr);
|
|
325
|
+
|
|
326
|
+
const resolveSpy = jest
|
|
327
|
+
.spyOn(StellarToml.Resolver, "resolve")
|
|
328
|
+
.mockResolvedValue({
|
|
329
|
+
URI_REQUEST_SIGNING_KEY: testKp1.publicKey,
|
|
330
|
+
});
|
|
331
|
+
// jest.spyOn() re-spying on an already-mocked function preserves the
|
|
332
|
+
// existing call history, so clear it to accurately assert non-invocation.
|
|
333
|
+
resolveSpy.mockClear();
|
|
334
|
+
|
|
335
|
+
expect(await uri.verifySignature()).toBe(false);
|
|
336
|
+
expect(resolveSpy).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
229
338
|
});
|
|
230
339
|
|
|
231
340
|
describe("Sep7Tx", () => {
|
|
@@ -589,6 +698,32 @@ describe("sep7Parser", () => {
|
|
|
589
698
|
).toBe(true);
|
|
590
699
|
});
|
|
591
700
|
|
|
701
|
+
it("isValidSep7Uri(uri) returns true when 'origin_domain' is absent", () => {
|
|
702
|
+
expect(
|
|
703
|
+
isValidSep7Uri(
|
|
704
|
+
"web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens",
|
|
705
|
+
).result,
|
|
706
|
+
).toBe(true);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("isValidSep7Uri(uri) returns true when 'origin_domain' is a valid fully qualified domain name", () => {
|
|
710
|
+
expect(
|
|
711
|
+
isValidSep7Uri(
|
|
712
|
+
"web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens&origin_domain=someDomain.com",
|
|
713
|
+
).result,
|
|
714
|
+
).toBe(true);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("isValidSep7Uri(uri) returns 'false' with 'reason' when 'origin_domain' is not a valid fully qualified domain name", () => {
|
|
718
|
+
const validation = isValidSep7Uri(
|
|
719
|
+
"web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens&origin_domain=stellar.org%40evil.com",
|
|
720
|
+
);
|
|
721
|
+
expect(validation.result).toBe(false);
|
|
722
|
+
expect(validation.reason).toBe(
|
|
723
|
+
"the provided 'origin_domain' parameter is not a valid fully qualified domain name",
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
|
|
592
727
|
it("isValidSep7Uri(uri) returns 'false' with 'reason' when it is not valid in some way", () => {
|
|
593
728
|
const validation1 = isValidSep7Uri(
|
|
594
729
|
"not-a-stellar-uri:tx?xdr=AAAAAP%2Byw%2BZEuNg533pUmwlYxfrq6%2FBoMJqiJ8vuQhf6rHWmAAAAZAB8NHAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAYAAAABSFVHAAAAAABAH0wIyY3BJBS2qHdRPAV80M8hF7NBpxRjXyjuT9kEbH%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAAAAAAAAA%3D&callback=url%3Ahttps%3A%2F%2FsomeSigningService.com%2Fa8f7asdfkjha&pubkey=GAU2ZSYYEYO5S5ZQSMMUENJ2TANY4FPXYGGIMU6GMGKTNVDG5QYFW6JS&msg=order%20number%2024",
|
|
@@ -834,7 +969,9 @@ describe("sep7Parser", () => {
|
|
|
834
969
|
|
|
835
970
|
it("Sep7Tx.getReplacements() throws on replace param missing hints", () => {
|
|
836
971
|
const uri = new Sep7Tx(
|
|
837
|
-
`web+stellar:tx?xdr=test&replace=${encodeURIComponent(
|
|
972
|
+
`web+stellar:tx?xdr=test&replace=${encodeURIComponent(
|
|
973
|
+
"sourceAccount:X",
|
|
974
|
+
)}`,
|
|
838
975
|
);
|
|
839
976
|
|
|
840
977
|
expect(() => uri.getReplacements()).toThrow(Sep7InvalidUriError);
|