mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-11 00:18:55 -05:00
[GH-ISSUE #1517] Security Issue with Enumerating Resources by ID in the URL #10696
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
@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?
@curious-debug commented on GitHub (Sep 22, 2025):
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.
@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"
@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]?...
@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
@curious-debug commented on GitHub (Sep 22, 2025):
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.
@oschwartz10612 commented on GitHub (Sep 23, 2025):
Agreed will probably try to replace with random strings. On the road map/.
@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.
@curious-debug commented on GitHub (Sep 23, 2025):
@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
resourcesin the sqlite db. Theresources.niceIdproperty is generated by functiongetUniqueSiteResourceNameinserver/db/names.ts, and theniceIdproperty is intended to be unique. These "random strings" are generated by combining the descriptors and names as listed inserver/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.niceIdas 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.jsoncontain: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:
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.guidand use a 32-hex-char as ID the URL, e.g. /auth/resource/a69ee64219414596a575bf7ed0443fe9Minor data hygiene note
I found an extra trailing-space in the word Northern Short-Tailed Shrew on line 1447 in
names.json.@Esa-mimbias commented on GitHub (Sep 24, 2025):
hi
may also/rather use a UUID than a 32-hex-char
@miloschwartz commented on GitHub (Sep 28, 2025):
Thanks everyone. Moved to GUID. Will be released in next version
@wallacebrf commented on GitHub (Sep 28, 2025):
Thanks for addressing the issue so quickly 😃
@curious-debug commented on GitHub (Sep 30, 2025):
Great! Looking forward to the update @miloschwartz and @oschwartz10612 ! Thanks so much for being so responsive.