[GH-ISSUE #1517] Security Issue with Enumerating Resources by ID in the URL #8699

Closed
opened 2026-04-30 04:42:47 -05:00 by GiteaMirror · 13 comments
Owner

Originally created by @curious-debug on GitHub (Sep 22, 2025).
Original GitHub issue: https://github.com/fosrl/pangolin/issues/1517

Originally assigned to: @miloschwartz on GitHub.

There is a security issue where the public can easily enumerate all resources by incrementing the integer ID in the /auth/resource/[ID] URL.

This was discussed in the #development channel in Discord with Owen (Core Maintainer) saying on 9/20/25, 11:29 AM: "Thanks this is definitely a valid concern!" Got a lot of agreement as well.

====
Guessing resources by enumerating IDs in the URL is quite simple. For example, when visiting a resource, the browser redirects to a URL that looks like this: https://[pangolin.fossorial.io / SelfHostedDomain]/auth/resource/[ID]?redirect=[resourceURL], where [ID] is the integer showing the order in which the resource was created.

One can simply remove the "redirect" querystring from the URL and just cycle through the [ID]s, which are integers, and all the resources that are available on the system can be probed.

Even worse, if one of the resources doesn't have authentication, then visiting the URL takes the person directly to the resource.

One major benefit of a Pangolin-type system is no longer needing to open ports on a firewall (which can be scanned), and with this issue in Pangolin, enumerating resources by ID presents the same ease of probing "Pangolin's firewall ports", if you will. For example, visiting these URLs reveal much. I could go on, but this makes the point well.

https://pangolin.fossorial.io/auth/resource/32 (PIN)
https://pangolin.fossorial.io/auth/resource/33 (PIN)
https://pangolin.fossorial.io/auth/resource/36 (email OTP)
https://pangolin.fossorial.io/auth/resource/38 (Platform SSO)
https://pangolin.fossorial.io/auth/resource/40 (PIN)
https://pangolin.fossorial.io/auth/resource/42 (PIN)
https://pangolin.fossorial.io/auth/resource/43 -> no auth -> https://diffkeep.hostlocal.app/
https://pangolin.fossorial.io/auth/resource/ ...

Rather than redirect visitors of a resource to https://[pangolin.fossorial.io / SelfHostedDomain] / auth/resource/[ID]?..., I would hope that the development team could program a better option—redirecting not to a URL with an incremental integer, but to a URL that perhaps has the "Resource" name that consists of the random words that are generated for created resources, e.g. "noisy-yellow-spotted-hamster".

Or a GUID would be even better.

For example, these are much harder to guess than https://[SelfHostedDomain]/auth/resource/1, /2, etc.:
https://[SelfHostedDomain]/auth/resource/noisy-yellow-spotted-hamster
https://[SelfHostedDomain]/auth/resource/5d66217c51344e29bc238348b3e20fa6

Originally created by @curious-debug on GitHub (Sep 22, 2025). Original GitHub issue: https://github.com/fosrl/pangolin/issues/1517 Originally assigned to: @miloschwartz on GitHub. There is a security issue where the public can easily enumerate all resources by incrementing the integer ID in the /auth/resource/[ID] URL. ================= This was discussed in the #development channel in Discord with Owen (Core Maintainer) saying on 9/20/25, 11:29 AM: "Thanks this is definitely a valid concern!" Got a lot of agreement as well. ==== Guessing resources by enumerating IDs in the URL is quite simple. For example, when visiting a resource, the browser redirects to a URL that looks like this: https://[pangolin.fossorial.io / SelfHostedDomain]/auth/resource/[ID]?redirect=[resourceURL], where [ID] is the integer showing the order in which the resource was created. One can simply remove the "redirect" querystring from the URL and just cycle through the [ID]s, which are integers, and all the resources that are available on the system can be probed. Even worse, if one of the resources doesn't have authentication, then visiting the URL takes the person directly to the resource. One major benefit of a Pangolin-type system is no longer needing to open ports on a firewall (which can be scanned), and with this issue in Pangolin, enumerating resources by ID presents the same ease of probing "Pangolin's firewall ports", if you will. For example, visiting these URLs reveal much. I could go on, but this makes the point well. https://pangolin.fossorial.io/auth/resource/32 (PIN) https://pangolin.fossorial.io/auth/resource/33 (PIN) https://pangolin.fossorial.io/auth/resource/36 (email OTP) https://pangolin.fossorial.io/auth/resource/38 (Platform SSO) https://pangolin.fossorial.io/auth/resource/40 (PIN) https://pangolin.fossorial.io/auth/resource/42 (PIN) https://pangolin.fossorial.io/auth/resource/43 -> no auth -> https://diffkeep.hostlocal.app/ https://pangolin.fossorial.io/auth/resource/ ... Rather than redirect visitors of a resource to https://[pangolin.fossorial.io / SelfHostedDomain] / auth/resource/[ID]?..., I would hope that the development team could program a better option—redirecting **not** to a URL with an incremental integer, but to a URL that perhaps has the "Resource" name that consists of the random words that are generated for created resources, e.g. "noisy-yellow-spotted-hamster". Or a GUID would be even better. For example, these are much harder to guess than https://[SelfHostedDomain]/auth/resource/1, /2, etc.: https://[SelfHostedDomain]/auth/resource/noisy-yellow-spotted-hamster https://[SelfHostedDomain]/auth/resource/5d66217c51344e29bc238348b3e20fa6
GiteaMirror added the ImprovementSecurity labels 2026-04-30 04:42:47 -05:00
Author
Owner

@wallacebrf commented on GitHub (Sep 22, 2025):

does this affect only the cloud based versions, or does this affect all pangolin installations?

i ask as you state
https://[pangolin.fossorial.io / SelfHostedDomain]/auth/resource/[ID]?redirect=[resourceURL]

which has the https://[pangolin.fossorial.io in the URL, which it is my understanding is only used when using the cloud service?

<!-- gh-comment-id:3320922695 --> @wallacebrf commented on GitHub (Sep 22, 2025): does this affect only the cloud based versions, or does this affect all pangolin installations? i ask as you state https://[pangolin.fossorial.io / SelfHostedDomain]/auth/resource/[ID]?redirect=[resourceURL] which has the https://[pangolin.fossorial.io in the URL, which it is my understanding is only used when using the cloud service?
Author
Owner

@curious-debug commented on GitHub (Sep 22, 2025):

does this affect only the cloud based versions, or does this affect all pangolin installations?
i ask as you state https://[pangolin.fossorial.io / SelfHostedDomain]/auth/resource/[ID]?redirect=[resourceURL]
which has the https://[pangolin.fossorial.io in the URL, which it is my understanding is only used when using the cloud service?

The links above show this affects the cloud-based version.
This affects my self-hosted installation as well.
So, this appears to affect all pangolin installations.

<!-- gh-comment-id:3320974496 --> @curious-debug commented on GitHub (Sep 22, 2025): > does this affect only the cloud based versions, or does this affect all pangolin installations? > i ask as you state https://[pangolin.fossorial.io / SelfHostedDomain]/auth/resource/[ID]?redirect=[resourceURL] > which has the https://[pangolin.fossorial.io in the URL, which it is my understanding is only used when using the cloud service? The links above show this affects the cloud-based version. This affects my self-hosted installation as well. So, this appears to affect **all pangolin installations**.
Author
Owner

@wallacebrf commented on GitHub (Sep 22, 2025):

interesting. i was asking as i tried to go to
https://pangolin.mydomain.com/auth/resource/x
and no matter what number i used between 0 and 20 (stopped after 20) i only ever got (as i would expect) server not found responses.

is there something else i need to do, to investigate this issue on my own system?

thanks!

edit, i should note that "https://pangolin.mydomain.com" is the way i access my resources, so for example if i wanted to access plex, i used "https://plex.pangolin.mydomain.com"

<!-- gh-comment-id:3321558681 --> @wallacebrf commented on GitHub (Sep 22, 2025): interesting. i was asking as i tried to go to https://pangolin.mydomain.com/auth/resource/x and no matter what number i used between 0 and 20 (stopped after 20) i only ever got (as i would expect) server not found responses. is there something else i need to do, to investigate this issue on my own system? thanks! edit, i should note that "https://pangolin.mydomain.com" is the way i access my resources, so for example if i wanted to access plex, i used "https://plex.pangolin.mydomain.com"
Author
Owner

@curious-debug commented on GitHub (Sep 22, 2025):

Then your first 20 resources have already been deleted. To find the current ID:
Create a new resource and have Platform SSO authentication on it. Open in an incognito tab. Observe the URL. You will see https://[whateverYourDomainNameIs]/auth/resource/[whatever-Incremental-ID-Your-System-Is-Currently-On]?...

<!-- gh-comment-id:3321581664 --> @curious-debug commented on GitHub (Sep 22, 2025): Then your first 20 resources have already been deleted. To find the current ID: Create a new resource and have Platform SSO authentication on it. Open in an incognito tab. Observe the URL. You will see https://[whateverYourDomainNameIs]/auth/resource/**[whatever-Incremental-ID-Your-System-Is-Currently-On]**?...
Author
Owner

@wallacebrf commented on GitHub (Sep 22, 2025):

yep, i see the issue i was having, i needed to do

https://dash.pangolin.mydomain/auth/resource/x and then as you said i was able to enumerate through the different things i have set up

Edit:
So I am local only, and am not using the cloud services offered by pangolin at all

I would agree this is a security concern if anything is not protected by auth

<!-- gh-comment-id:3321615971 --> @wallacebrf commented on GitHub (Sep 22, 2025): yep, i see the issue i was having, i needed to do https://dash.pangolin.mydomain/auth/resource/x and then as you said i was able to enumerate through the different things i have set up Edit: So I am local only, and am not using the cloud services offered by pangolin at all I would agree this is a security concern if anything is not protected by auth
Author
Owner

@curious-debug commented on GitHub (Sep 22, 2025):

I would agree this is a security concern if anything is not protected by auth

It's a security concern even if a resource is still protected by auth. — by simply enumerating IDs, intruders will know which resources are active on your system (or pangolin's cloud system), and they can begin their attack. If they find one with no auth (or one with weak auth), then they are right in, just as if you had an open port on your firewall. Security by obscurity is why using something like GUIDs is way better than integer-based IDs in public facing URLs — doing so makes it almost impossible for intruders to enumerate through a list of IDs, where weak links (no pun intended) can be discovered and brute forced.

Under the current codebase, if an intruder gets access to a legitimate user's browser history, they will then have easy awareness of where to begin the enumeration attack, exposing all resources, just based on that one ID. If that ID was a GUID, it would be much, much harder to attack the other resources.

<!-- gh-comment-id:3321746029 --> @curious-debug commented on GitHub (Sep 22, 2025): > I would agree this is a security concern if anything is not protected by auth It's a security concern even if a resource **is still** protected by auth. — by simply enumerating IDs, intruders will know which resources are active on your system (or pangolin's cloud system), and they can begin their attack. If they find one with no auth (or one with weak auth), then they are right in, just as if you had an open port on your firewall. Security by obscurity is why using something like GUIDs is way better than integer-based IDs in public facing URLs — doing so makes it almost impossible for intruders to enumerate through a list of IDs, where weak links (no pun intended) can be discovered and brute forced. Under the current codebase, if an intruder gets access to a legitimate user's browser history, they will then have easy awareness of where to begin the enumeration attack, exposing all resources, just based on that one ID. If that ID was a GUID, it would be much, much harder to attack the other resources.
Author
Owner

@oschwartz10612 commented on GitHub (Sep 23, 2025):

Agreed will probably try to replace with random strings. On the road map/.

<!-- gh-comment-id:3322184755 --> @oschwartz10612 commented on GitHub (Sep 23, 2025): Agreed will probably try to replace with random strings. On the road map/.
Author
Owner

@curious-debug commented on GitHub (Sep 23, 2025):

Thanks for putting this on roadmap @oschwartz10612 and @miloschwartz.

Please also look at
Feature Request - Authentication-Rule Sets
and
New Rule - Always Deny if NOT IP/IP Range Address Match

I would contribute code, but I'm not familiar enough with this codebase. Happy to support financially though! This is a great project to get behind. Love having some independence from CloudFlare Tunnels and the privacy of self-hosting. Really wish geoblocking worked for self-host. Don't really understand the rationale behind "we need to host services to resolve IP addresses and evolve it quickly as network change", because we can host and resolve IPs ourselves, no? Couldn't self-host version get geoblocking updates from pangolin via an exposed API? I understand we can do Traefik, but that isn't managed via web interface, so would love to have geoblocking included with self-hosted version.

<!-- gh-comment-id:3325091179 --> @curious-debug commented on GitHub (Sep 23, 2025): Thanks for putting this on roadmap @oschwartz10612 and @miloschwartz. Please also look at [Feature Request - Authentication-Rule Sets](https://github.com/fosrl/pangolin/issues/1523) and [New Rule - Always Deny if NOT IP/IP Range Address Match](https://github.com/fosrl/pangolin/issues/1524) I would contribute code, but I'm not familiar enough with this codebase. Happy to support financially though! This is a great project to get behind. Love having some independence from CloudFlare Tunnels and the privacy of self-hosting. Really wish geoblocking worked for self-host. Don't really understand the rationale behind "we need to host services to resolve IP addresses and evolve it quickly as network change", because we can host and resolve IPs ourselves, no? Couldn't self-host version get geoblocking updates from pangolin via an exposed API? I understand we can do Traefik, but that isn't managed via web interface, so would love to have geoblocking included with self-hosted version.
Author
Owner

@curious-debug commented on GitHub (Sep 23, 2025):

Agreed will probably try to replace with random strings. On the road map/.

@oschwartz10612 and @miloschwartz :

TLDR; Please do not use random strings, e.g. resources.niceId, as the ID in the URL. A better security posture would be to create a new field, e.g. resources.guid, and use a 32-hex-char as ID the URL, e.g. /auth/resource/a69ee64219414596a575bf7ed0443fe9. See below for explanation.

I inspected the table resources in the sqlite db. The resources.niceId property is generated by function getUniqueSiteResourceName in server/db/names.ts, and the niceId property is intended to be unique. These "random strings" are generated by combining the descriptors and names as listed in server/db/names.json, and then checked to see if they are unique.

Assuming you intend to use, not GUIDs, but these "random strings" in resources.niceId as the ID in the URL — e.g. /auth/resource/adorable-platypusI strongly advise that you do not. There are problems with this, as compared to using GUIDs.

1. Low entropy means easier attack surface.

The names in the file server/db/names.json contain:

  • Descriptors: 1,347 entries, 1,339 unique (duplicates include: adorable, dismal, distant, downright, dreary, oblong, precious, tremendous).
  • Animals: 358 unique entries.

Total unique adjective+animal names: 1,339 × 358 = 479,362 possibilities. Entropy of this scheme = log₂(479,362) ≈ 19 bits.

With 479,362 possible names, there is:

  • ~1% chance of at least one collision after about 98 assignments.
  • ~10% after about 318 assignments.
  • ~50% after about 815 assignments.

The most important bit: If collisions occur so easily, imagine how trivial it would be for an attacker to discover/guess/brute them as well. This is no better than port scanning. 479,362 possibilities is only 7 times more than a port scan of 65536 ports. Especially if these words are publicly known, as they are here in github.

2. High entropy means reduced attack surface.

A 32-hex-char ID — e.g. a69ee64219414596a575bf7ed0443fe9 — has 128 bits of entropy.
Total unique IDs: 16^32 = 3.40 × 10³⁸ possibilities. Entropy is 128 bits.

Here, collision probabilities at any sane scale are effectively zero, meaning, practically impossible for an attacker to discover/guess/brute IDs, such that the attacker will just move on.

Bottom line: Human-friendly names (niceIds) are great as labels—but not as identifiers. GUIDs eliminate collision risk and reduce attack surface.

Please create a new field resources.guid and use a 32-hex-char as ID the URL, e.g. /auth/resource/a69ee64219414596a575bf7ed0443fe9


Minor data hygiene note

I found an extra trailing-space in the word Northern Short-Tailed Shrew on line 1447 in names.json.

<!-- gh-comment-id:3325772594 --> @curious-debug commented on GitHub (Sep 23, 2025): > Agreed will probably try to replace with `random strings`. On the road map/. @oschwartz10612 and @miloschwartz : ### TLDR; Please do not use random strings, e.g. `resources.niceId`, as the ID in the URL. A better security posture would be to create a new field, e.g. `resources.guid`, and use a 32-hex-char as ID the URL, e.g. /auth/resource/**a69ee64219414596a575bf7ed0443fe9**. See below for explanation. I inspected the table `resources` in the sqlite db. The `resources.niceId` property is generated by function `getUniqueSiteResourceName` in `server/db/names.ts`, and the `niceId` property is intended to be unique. These "random strings" are generated by combining the descriptors and names as listed in `server/db/names.json`, and then checked to see if they are unique. Assuming you intend to use, **not GUIDs**, but these "random strings" in `resources.niceId` as the ID in the URL — e.g. /auth/resource/**adorable-platypus** — **_I strongly advise that you do not._** There are problems with this, as compared to using GUIDs. ### 1. Low entropy means easier attack surface. The names in the file `server/db/names.json` contain: - Descriptors: 1,347 entries, 1,339 unique (duplicates include: adorable, dismal, distant, downright, dreary, oblong, precious, tremendous). - Animals: 358 unique entries. Total unique adjective+animal names: 1,339 × 358 = 479,362 possibilities. Entropy of this scheme = log₂(479,362) ≈ 19 bits. With 479,362 possible names, there is: - ~1% chance of at least one collision after about 98 assignments. - ~10% after about 318 assignments. - ~50% after about 815 assignments. > **The most important bit: _If collisions occur so easily, imagine how trivial it would be for an attacker to discover/guess/brute them as well. This is no better than port scanning. 479,362 possibilities is only 7 times more than a port scan of 65536 ports. Especially if these words are publicly known, as they are here in github._** ### 2. High entropy means reduced attack surface. A 32-hex-char ID — e.g. a69ee64219414596a575bf7ed0443fe9 — has 128 bits of entropy. Total unique IDs: 16^32 = 3.40 × 10³⁸ possibilities. Entropy is 128 bits. Here, collision probabilities at any sane scale are effectively zero, meaning, practically impossible for an attacker to discover/guess/brute IDs, such that the attacker will just move on. ### Bottom line: Human-friendly names (niceIds) are great as labels—but not as identifiers. GUIDs eliminate collision risk and reduce attack surface. Please create a new field `resources.guid` and use a 32-hex-char as ID the URL, e.g. /auth/resource/**a69ee64219414596a575bf7ed0443fe9** ---- ### Minor data hygiene note I found an extra trailing-space in the word [Northern Short-Tailed Shrew ](https://github.com/fosrl/pangolin/blob/b63bffa524f9519e1a0bf6d4db8b5a72f95ddd81/server/db/names.json#L1447) on line 1447 in `names.json`.
Author
Owner

@Esa-mimbias commented on GitHub (Sep 24, 2025):

hi
may also/rather use a UUID than a 32-hex-char

<!-- gh-comment-id:3326841384 --> @Esa-mimbias commented on GitHub (Sep 24, 2025): hi may also/rather use a UUID than a 32-hex-char
Author
Owner

@miloschwartz commented on GitHub (Sep 28, 2025):

Thanks everyone. Moved to GUID. Will be released in next version

<!-- gh-comment-id:3344393306 --> @miloschwartz commented on GitHub (Sep 28, 2025): Thanks everyone. Moved to GUID. Will be released in next version
Author
Owner

@wallacebrf commented on GitHub (Sep 28, 2025):

Thanks for addressing the issue so quickly 😃

<!-- gh-comment-id:3344406316 --> @wallacebrf commented on GitHub (Sep 28, 2025): Thanks for addressing the issue so quickly 😃
Author
Owner

@curious-debug commented on GitHub (Sep 30, 2025):

Great! Looking forward to the update @miloschwartz and @oschwartz10612 ! Thanks so much for being so responsive.

<!-- gh-comment-id:3350109358 --> @curious-debug commented on GitHub (Sep 30, 2025): Great! Looking forward to the update @miloschwartz and @oschwartz10612 ! Thanks so much for being so responsive.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/pangolin#8699