@Lorimer, in my opinion you’re not missing some hidden security mechanism here. There isn’t one.
DFINITY’s own security guidance actually documents this exact scenario. I think the more useful thing is to make the OISY case concrete rather than keep talking about it as a hypothetical, because the repository shows something stronger than what has been assumed in this thread.
1. The documented position
The Security best practices → Decentralization / Canister control page is pretty explicit about this: applications are often accessed through a custom domain registered with a DNS registrar by one of the developers. That developer can point the domain to a completely different web application, potentially not hosted on ICP at all, and users will still trust whatever is served from that domain.
The same documentation points out the important Internet Identity part: the session key for that origin is stored in the browser, so malicious JavaScript served from the same origin can control that session and anything the session is allowed to do.
So when you ask “what am I missing?”, my answer is basically: nothing. This is a known centralisation point. The problem is that most users are never really told that the trust model works this way.
2. Native canister URL vs custom domain
There is a very simple solution if the goal is to avoid this particular class of problem: use the native canister URL and don’t put a custom domain in the derivation path.
I don’t think we should pretend this is some unsolved problem.
There is still a tradeoff, obviously. A native canister URL is ugly, harder to remember and worse for UX. But from a security perspective, it removes the per-application DNS dependency.
With a custom domain, the binding depends on DNS records controlled by whoever owns the domain: a CNAME to <domain>.icp1.io, a TXT record _canister-id.<domain> containing the canister ID, a CNAME for _acme-challenge.<domain> to .icp2.io, plus the .well-known/ic-domains file in the canister.
The canister file proves that the canister agrees to serve that domain. The TXT record proves that the DNS owner agrees. Those are two different things.
Change the DNS records and you can send users somewhere else. You can even remove the ICP records completely, point the domain at another server and obtain a valid certificate for the same domain. At that point ICP isn’t involved anymore. There is no consensus, no proposal and no on-chain record saying “this domain changed hands”.
With the native canister URL, the hostname itself contains the canister ID. If you want to serve a different application there, you actually need control over that canister. That’s a meaningful security improvement.
There is still a residual trust assumption, because response verification happens at the HTTP gateway rather than directly in the browser. The gateway checks the BLS signature and witness, and the browser receives the already verified response. So users still rely on DNS and TLS for the shared icp0.io / icp.net namespace, as well as the gateway implementation. But that’s a very different situation. With a custom domain, every application has its own off-chain DNS trust point. With a native canister URL, the DNS namespace is shared and much more constrained, while the canister ID is actually part of the thing being certified.
And yes, raw. bypasses certification entirely, with allow_raw_access defaulting to true. That’s another thing worth keeping in mind.
3. The OISY case
Looking at dfinity/oisy-wallet on main, src/frontend/src/lib/constants/app.constants.ts contains:
const IS_ICP_DOMAIN_URL = DOMAIN_URL_HOSTNAME.endsWith('.icp0.io');
export const AUTH_DERIVATION_ORIGIN =
IS_SIGNER_DOMAIN || BETA || (PROD && IS_ICP_DOMAIN_URL)
? STAGING ? 'https://tewsx-...-cai.icp0.io' : 'https://oisy.com'
: ...;
Then src/frontend/src/lib/utils/auth.utils.ts passes that derivationOrigin whenever the current origin appears in VITE_AUTH_ALTERNATIVE_ORIGINS, and scripts/build.ii-alternative-origins.mjs writes .well-known/ii-alternative-origins from the same variable.
SIGNER_DOMAINS.md also says it directly: the different domains derive identity from the same canonical origin as the main wallet, and production uses https://oisy.com.
So oisy.com isn’t just a nice front door for OISY; It is the principal derivation root.
The native canister URL is configured as an alternative origin of the custom domain, not the other way around. A user who deliberately opens cha4i-riaaa-aaaan-qeccq-cai.icp0.io can still end up with a principal derived from https://oisy.com.
That’s why imo the usual advice of “just establish your account from the native canister URL, like TAGGR does” doesn’t really apply to OISY. There isn’t a separate canister-derived account to move to.
If someone controls the domain, they don’t necessarily need to break Internet Identity itself. They can serve malicious JavaScript from the trusted origin.
There is another important detail here. When no derivationOrigin is supplied, II derives from event.origin directly, and II doesn’t require that origin to be hosted on ICP. So malicious JavaScript served from https://oisy.com, with a perfectly valid certificate, can request the same derivation origin as the real OISY.
That means the resulting principals are the same. Same backend accounts, same chain-fusion addresses.
To be fair, i think this is clearly a deliberate UX decision. It allows the signer subdomains and the PWA to behave as one wallet instead of creating multiple identities. And the canister side isn’t just sitting there uncontrolled. Production upgrades go through an Orbit station approval process with reproducible Docker builds and reviewer validation scripts, as described in HACKING.md and SIGNER_DOMAINS.md.
That’s actually the interesting asymmetry:
Changing the application code requires an on-chain governance action. Changing where oisy.com points requires access to a registrar or DNS account.
so those are very different security models.
a smaller thing also caught my attention while looking through it.
IS_ICP_DOMAIN_URL checks .icp0.io specifically. That’s probably worth another look now that icp.net is the default gateway domain for new frontend canisters.
4. The “obvious” fix
There is a tempting configuration change here: make the canister origin the derivation origin and make the custom domain the alternative origin.
But there is one important caveat; simply switching an existing application from a custom domain to its native canister domain as the derivation origin is not a trivial fix. The derivation origin is part of the Internet Identity principal derivation. Changing it means that existing users can end up with different principals, even if they authenticate with the same Internet Identity anchor. For an application that already has users and assets associated with those principals, that can turn into a significant migration problem.
So I wouldn’t suggest that existing dapps should simply change their derivation origin overnight.
For new applications, however, I think this is something that could reasonably be documented as a security best practice.
If a dapp chooses to use Internet Identity alternative origins, the native canister domain should preferably be used as the canonical derivation origin, with custom domains configured as alternative origins.
That way, the application’s identity is rooted in the canister rather than in a DNS-controlled domain from the beginning. The custom domain can still provide the UX and branding people want, but losing or compromising that domain doesn’t become equivalent to losing the application’s identity root.
For existing applications already using a custom domain as their derivation origin, the situation is more complicated because changing it can change every user’s principal. That is exactly why this is much easier to get right at the architecture stage than to fix later.
By the way, unfortunately that doesn’t actually solve the problem.
If oisy.com remains an accepted alternative origin, an attacker controlling oisy.com can still serve a page that points back to the canister origin. II can fetch /.well-known/ii-alternative-origins from the canister, see that oisy.com is an allowed origin, and accept it.
So the real boundary is not the order in which the origins are configured.
If a DNS-controlled domain remains part of the derivation set, control of that DNS remains equivalent to control of the identity derived from that origin.
Removing the custom domain entirely is what restores the stronger property.
That is why I don’t think this is something you can solve with one more configuration flag inside OISY. You either use only the native canister origin, or you need something outside the normal application configuration, such as a client that pins the canister ID, an extension/native application, a self-hosted gateway, or some additional canister-side security policy that makes possession of a frontend session insufficient by itself.
For example, spending limits, withdrawal allowlists, timelocks or a second factor bound to a key that the frontend never controls.
Those approaches make the security property independent of the hostname.
One practical thing for developers: if a Web2 domain is about to expire or you’re no longer going to control it, remember to remove it from the Internet Identity alternative origins first. Otherwise, whoever acquires the domain later may still be able to use it as an accepted origin.
5. DNS hijacking is another problem
There is also a related issue that I think deserves to be separated from simple domain expiry or someone intentionally changing the DNS.
Even if the legitimate owner renews the domain and never decides to repoint it, DNS itself can become the attack surface.
If an attacker compromises the registrar account, DNS provider account, a delegated DNS provider, or some other part of the DNS management chain, they can potentially hijack the domain without ever compromising the ICP canister.
From the user’s perspective, nothing necessarily looks broken.
The URL is still oisy.com.
The TLS certificate can still be valid.
The browser shows the same domain.
The attacker simply controls what that domain resolves to.
This is one of the reasons I had actually thought about building a small “DNS Guardian” in the past. The idea was basically to continuously monitor the DNS records of important ICP domains and compare them against an expected on-chain/canister configuration, with alerts when something changes. I never had the time to take it anywhere though.
And honestly, I’m not even sure there is a product there that people would pay for. It would probably require continuous inter-canister calls and monitoring infrastructure, and “DNS Guardian for paranoid ICP users” isn’t exactly the catchiest product pitch.
But for this particular threat model, being a little paranoid is not completely irrational 
The interesting thing is that DNS changes are one of the few parts of the whole chain where you can have a perfectly valid ICP application on one side and still silently redirect users somewhere completely different before they ever reach it.
6. Regarding the questions
Questions 1 to 4 are ultimately things DFINITY would need to answer, I would support asking them.
I’d probably add one more:
Would the custom-domain service consider supporting an on-chain-attested canister/domain binding instead of relying on a DNS TXT record that the domain owner can change at any time?
That wouldn’t solve every DNS problem, but it would at least move the binding itself into something that ICP can attest to.
And regarding the example you linked directly, yes, an expired domain is basically the same failure mode with a different trigger.
Pointing users to the native canister URL after the domain is lost is a useful mitigation, but it isn’t a design that prevents the problem in the first place. The domain didn’t need to be maliciously compromised. It simply expired. That alone created a period in which somebody else could potentially register it and inherit the origin that users had been using for Internet Identity and that’s the part I think users should understand.
7. Why the native domain is genuinely safer on ICP
I initially described the native canister URL as a reduction in risk rather than an elimination of it. That’s technically correct, but I think it misses the most important point about why ICP is different here.
The canister ID participates in the cryptographic verification chain. A domain name doesn’t.
Every certified response contains an IC-Certificate. According to the HTTP gateway protocol specification, that certificate is signed by the NNS root key or by a subnet delegation ultimately signed by that root key.
The interface specification makes the delegation model even more explicit: delegations are scoped using canister_ranges, and the relevant canister ID has to fall inside the range.
So when you see:
cha4i-riaaa-aaaan-qeccq-cai.icp0.io
the canister ID isn’t just an arbitrary string in the URL. It is a checkable part of the certification chain.
Given the NNS root key, you can verify that the response was certified for that particular canister.
oisy.com doesn’t appear in that certificate, the state tree or an NNS registry entry.
Its binding comes from DNS and WebPKI.
The .well-known/ic-domains file doesn’t change that. It says that the canister is willing to serve that domain. It doesn’t prove that the domain currently resolves to that canister.
On the normal web, the domain is the best identity we have, so everyone builds around it. On ICP, there is the canister ID. Using a custom domain effectively puts a Web2 name in front of a Web3 object that already has a cryptographically verifiable identity.
If the goal is maximum security, the simplest answer is therefore also the least attractive one from a UX perspective:
use the native canister domain.
If you want the custom domain, you’re choosing convenience and branding in exchange for an additional off-chain trust assumption.
8. Certification doesn’t protect you if DNS sends you somewhere else
This is another point that gets mixed up quite often.
People say “but ICP responses are certified”.
Yes, they are.
But certification doesn’t prove that DNS sent you to the correct canister.
The gateway can correctly verify a response from the canister it was asked to reach. If DNS has already been changed so that oisy.com points somewhere else, you’re no longer asking the gateway for the legitimate OISY canister.
The attacker can serve a completely legitimate ICP canister from their own infrastructure. The response can be correctly certified, the certificate can be valid, the canister can really exist on ICP.
None of that tells you that oisy.com was supposed to point there. So the certification mechanism protects the response. It doesn’t magically make DNS trustworthy. This is also why the native canister URL matters.
9. The trust set is different
This is probably the simplest way I can describe the difference.
With a custom domain, the things that can affect what users receive include the canister controllers, the registrar, the DNS provider, potentially whoever compromises those accounts, and the WebPKI/ACME side of obtaining certificates.
With the native canister URL, you remove the per-application registrar and DNS provider from the equation.
You still trust the gateway namespace and its DNS, so I wouldn’t call it trustless.
But the failure mode is different.
A compromise of one application’s custom domain can quietly affect that application.
A compromise of icp0.io or icp.net would potentially affect the whole ICP ecosystem at once. That’s obviously a huge target, but it is also a shared, highly visible target. Changes are much harder to hide.
That’s not “decentralised DNS”.
It’s a smaller and much more observable trust assumption.
And importantly, canister controllers are at least something you can inspect. Under SNS, NNS or an Orbit station, code changes can be associated with governance actions and wasm hashes.
A registrar login doesn’t have that property.
10. Why this matters even more with Internet Identity
This matters even more when the domain is used as the Internet Identity derivation origin.
A principal derived from the native canister origin is tied to that canister, while a principal derived from https://oisy.com is tied to that web origin. If someone gains control of the domain, they don’t need to break Internet Identity or steal a seed phrase. They only need to become the origin the browser believes is https://oisy.com.
Because OISY is a wallet, that’s not just an application routing issue. The origin is part of the identity users use to access their assets.
The practical attack still requires users to reach the compromised origin.
If they already have a valid session, malicious JavaScript may be able to use that existing session without another login. If they don’t, they may simply go through the normal Internet Identity flow, seeing the same domain, TLS certificate and potentially even application metadata they normally expect.
There is also a more subtle point in the II implementation: certification protects the response after the canister has been resolved, but resolving a custom domain to a canister can still involve the domain itself. So if DNS is compromised, the pointer to the canister can potentially be compromised before certification even enters the picture.
This makes the attack more of a harvesting window than an instant private-key-style compromise. The attacker needs users to arrive, but the user may receive very little indication that anything has changed.
That’s why using the native canister domain as the canonical derivation origin is the cleaner security model for new applications. Custom domains can still be used as alternative origins, but then DNS becomes part of the security boundary.
And that’s really the question here: ICP can certify that a response came from a particular canister. It can’t, by itself, tell the browser that DNS sent it to the right place in the first place.
I’d be happy to see any corrections or clarifications from DFINITY here, as I may have misunderstood or overlooked some details in the Internet Identity implementation. In any case, I wouldn’t consider what I’ve described an open vulnerability in OISY. It’s ultimately a design choice: OISY chose a custom domain as its canonical derivation origin, and that comes with a different trust model. Whether that trade-off is worth it is a matter of security vs. UX, and reasonable people can disagree on it.