A health insurer sends an email once a year inviting members to check their contact details. The button leads to a CloudPage showing the postal address, the phone number and the email address, with a form for changes. The link was built quickly, a few days before the send, with the membership number placed straight into the address.
Three weeks later, a member writes to the data protection officer. He had spotted his number in the address bar, replaced it with a neighbouring number, and seen someone else's contact details appear. The numbers run in sequence, so anyone could browse the file.
The page worked perfectly in testing, and nothing in its code was wrong in technical terms. It read a parameter, looked up the matching row and displayed it. The flaw sits entirely in the trust placed in that parameter, and it counts as a personal data breach, with the documentation and notification duties that follow.
A URL does not stay in the browser of the person you sent it to. It passes through browsing history, forwarded emails, corporate proxy logs, analytics tools, and the header sent to the sites a page calls. Treat everything it carries as known to third parties.
Salesforce is blunt about it: do not pass a SubscriberID, a Subscriber Key or a Contact Key in clear text, and do not include personal data such as the email address in links.
| How the identifier is passed | Readable by a third party | Editable by hand | Verdict |
|---|---|---|---|
| Plain number in the query string | Yes | Yes | Never do this |
| Base64 or hexadecimal encoding | Yes, decoded in a second | Yes | Never do this, it is not encryption |
CloudPagesURL | No | No, an altered string will not decrypt | The recommended method from an email |
EncryptSymmetric with a managed key | No | No | For links built outside an email |
⚠️ Base64 is not encryption: Salesforce names that encoding and hexadecimal conversion as insufficient. An encoded identifier is decoded, incremented and re-encoded in seconds. The only thing you gain is a delay before the flaw is found.
The CloudPagesURL function takes the page identifier, then name and value pairs, and returns the address with an encrypted query string. According to the documentation, that string also carries a reference to the originating email, which makes personalisation strings usable on the page.
%%[
SET @adherentId = AttributeValue("AdherentId")
]%%
<a href="%%=RedirectTo(CloudPagesURL(4521, 'cle', @adherentId, 'v', '2026'))=%%">
Vérifier mes coordonnées
</a>
The cle parameter duplicates the context carried by the link, and that is deliberate: it will serve as a second check on the page. The v parameter identifies the campaign and lets you reject links that are too old. No data appears in clear text in the resulting address.
Some parameter names are reserved and cannot be used, among them PAGEID, MID, JID, LID, SID, JSB and URLID. Two notes for the other channels: MicrositeURL plays the same role for classic content microsites, and on an SMS or a push notification, the page returns an error when the recipient is not present in All Subscribers.
On the page side, two functions read the parameters received. The official documentation describes RequestParameter and QueryParameter in the same terms, and presents the second as kept for compatibility. Use the first everywhere, the code reads better. A missing value raises no error, the function returns an empty string, which is handy for testing and dangerous when the code moves straight on to a lookup without checking.
The Trailhead security module sets out simple rules that are enough to avoid the incident. All validation happens server side. A page that handles data requires authentication. You check with at least two parameters that the same subscriber is the one interacting with the page. A public page opens with a global check that stops everything when a required parameter is missing.
%%[
SET @cle = RequestParameter("cle")
SET @version = RequestParameter("v")
SET @ctxCle = _subscriberkey
SET @acces = "refuse"
SET @motif = ""
IF Empty(@cle) OR Empty(@ctxCle) THEN
SET @motif = "parametre_absent"
ELSEIF RegExMatch(@cle, "^[0-9]{6,10}$", 0) == "" THEN
SET @motif = "format_invalide"
ELSEIF @cle != @ctxCle THEN
SET @motif = "incoherence_contexte"
ELSEIF @version != "2026" THEN
SET @motif = "lien_perime"
ELSE
SET @rows = LookupRows("Adherents", "AdherentId", @cle)
IF RowCount(@rows) == 1 THEN
SET @acces = "ok"
SET @ligne = Row(@rows, 1)
ELSE
SET @motif = "adherent_inconnu"
ENDIF
ENDIF
]%%
Each condition has a precise job. The first blocks direct access to the page without a link. The second rejects a value that does not have the shape of an identifier. The third checks that the parameter and the context point to the same person, which is the check that was missing in the incident. The last confirms that the row exists, and only once.
When access is refused, keep the reason for the log and show a neutral message, identical whatever the reason. Telling the visitor on screen that a member cannot be found confirms to a third party whether an identifier exists, and echoes back a received value without processing. The log itself holds neither the key received nor the IP address: it exists to measure the volume of refusals by reason, not to identify people. A sharp rise in the context mismatch reason after a send points to a badly built link, while a rise in the format reason with no recent send points to someone probing the page.
💡 Display only what you have read: in the authorised branch, the page shows only values taken from the data extension, never values received in the request. A parameter echoed back as it arrived is the classic entry point for cross-site scripting.
Encryption stops someone forging a link or changing its contents. It does not stop them using it. An email forwarded to a colleague, a shared mailbox, a borrowed phone, and the page opens exactly as it does for the intended recipient. The design question is therefore not whether the link is encrypted, but what someone who holds the link without being the right person gets to see.
The answer dictates what the page contains. An encrypted link is fine for showing little data and for low stakes actions, communication preferences for example. For sensitive data or a high impact action, send people to the authenticated customer area or ask for a second check. In between, mask part of what you display, a phone number reduced to its last two digits is enough to confirm that it is up to date.
That leaves the HTTP layer, which is set at the top of the page and tested once. Salesforce recommends serving pages over HTTPS and adding security headers: Strict-Transport-Security with a duration, X-Frame-Options set to Deny, X-Content-Type-Options set to nosniff, and Referrer-Policy set to strict-origin-when-cross-origin, the last one being what keeps external sites from receiving the full page address. The Content-Security-Policy header deserves separate handling, because the most restrictive value also blocks scripts and stylesheets loaded from a CDN. List the domains the page needs first, test on a copy, then publish.
In an Adobe Campaign web application, preloading the recipient usually relies on an encrypted identifier in the link, and the page refuses access when decryption fails. CloudPagesURL does the same job on the parameter itself, which is neither readable nor editable.
The difference lies in the control. At Adobe, preloading is an option of the web application and the refusal is handled by the platform. In a CloudPage, nothing blocks the display on your behalf: when the string is empty, the code runs anyway and the page appears, blank, inviting someone to enter contact details that will be attached to nobody. The check at the top of the page is yours to write, like a validation in a server script.
Open one of your public CloudPages, look at its address, and change one character of the parameter. If the page shows anything other than your refusal message, you have the same flaw as the insurer. The test takes thirty seconds and is worth repeating after every release.
No: Salesforce says not to pass a SubscriberID, a Subscriber Key or a Contact Key in clear text, and not to include personal data such as the email address in links. A URL does not stay in the recipient's browser: it passes through browsing history, forwarded emails, corporate proxy logs, analytics tools and the header sent to the sites the page calls. Treat everything it carries as known to third parties.
No, it is not encryption. Salesforce names that encoding and hexadecimal conversion as insufficient. An encoded identifier is decoded, incremented and re-encoded in seconds. The only thing you gain is a delay before the flaw is found.
The CloudPagesURL function takes the page identifier, then name and value pairs, and returns the address with an encrypted query string. According to the documentation, that string also carries a reference to the originating email, which makes personalisation strings usable on the page. No data appears in clear text in the resulting address, and an altered string will not decrypt. For classic content microsites, MicrositeURL plays the same role.
The Trailhead security module requires that all validation happens server side, that you check with at least two parameters that the same subscriber is the one interacting with the page, and that a public page opens with a global check that stops everything when a required parameter is missing. In practice you refuse a missing parameter, an invalid format and a mismatch between the parameter and the context, then confirm the row exists, and only once. When access is refused, keep the reason for the log and show a neutral message, identical whatever the reason.
Encryption stops someone forging a link or changing its contents, but it does not stop them using it: a forwarded email, a shared mailbox or a borrowed phone, and the page opens exactly as it does for the intended recipient. The design question is therefore what someone who holds the link without being the right person gets to see. An encrypted link is fine for showing little data and for low stakes actions, while sensitive data or a high impact action call for the authenticated customer area or a second check.
I can audit your CloudPages, your outbound links and your access checks, then hand you a written diagnosis with the fixes to apply and the order to apply them in.
Tell me about your context →