ServiceNow OAuth Secrets AI Leaves in Plaintext
"The AI wired up the OAuth connection, it works in dev — but where did it actually put the secret?" That's the right question to be nervous about, because AI-built ServiceNow connectors optimize for a call that authenticates, not for a credential stored safely — one AI-built FSM integration we rebuilt shipped a recoverable plaintext OAuth secret among 17 findings6, and roughly 45% of AI code-generation tasks introduce a known security flaw1. An unread secret like that is an automatic cert finding and a client security-review failure, landing on your margin.
Key takeaways#
- The connector working in dev is exactly why nobody reviews where the secret landed — "it authenticates" is the false all-clear you have to stop trusting.
- A two-minute instance read tells you whether the credential is encrypted or recoverable — do it before cert review does it for you.
Where did the AI actually put the secret?#
It sat in the open — a recoverable plaintext string, not an encrypted credential — and the connector still authenticated cleanly in dev, which is exactly why nobody read it before shipping6. When I tore down that FSM integration, the OAuth secret wasn't hidden in some obscure corner; it was stored where a person could read it back in cleartext, and the token call worked anyway. That combination is the trap. A working call feels like proof the credential is fine.
Here's what made it invisible: the connection resolved, the outbound request came back 200, and the ATF suite was green6. Every surface a reviewer glances at said "done." Nothing about a live, authenticating connector tells you the secret behind it is recoverable rather than encrypted — you only learn that by opening the record that holds it and checking the field, which nobody did because the thing plainly worked.
So the placement wasn't exotic. The AI reached for whatever got the call to succeed fastest, dropped the value somewhere it could read it straight back, and moved on. The connector authenticating in dev didn't make the storage safe — it just made the danger quiet. If you let a model wire up an OAuth connection, assume the secret landed in the convenient spot, not the correct one, until you've looked.
Where should an OAuth secret live in ServiceNow?#
The correct home is oauth_entity.client_secret — a Password (2-Way Encrypted / password2) field, which stores the value encrypted and never renders it back in cleartext2. That's the whole point of the field type: even a reader with the record open sees a masked value, not the secret. If your connector's credential lives anywhere else, you're outside the sanctioned pattern, and ServiceNow's own guidance is blunt about it — don't keep credentials in plain text for outbound OAuth token calls3.
So when you go looking, you're really checking one thing: did the secret land in that encrypted field, or somewhere convenient the AI could read straight back? There are four convenient spots I see AI reach for, and all four fail a security review.
First, a plaintext string in sys_properties — the fastest place to stash a value the model can fetch on the next line. Second, a hardcoded literal inside a Script Include, sitting in source you can read at a glance. Third, the same thing tucked into a REST Message's Authorization header, so the credential ships in the message definition itself. Fourth — and this one's sneaky because it looks legitimate — a non-encrypted field on a connection & credential alias record. The alias pattern is the right pattern, but only if the value behind it is actually stored encrypted4. An alias pointing at a cleartext field is a plaintext secret wearing the right costume.
Notice what unites all four: the value stays recoverable. That's the tell. The encrypted password2 field is the only home where "I can read this back" is false — which is exactly why it's the one place you want the secret to be, and the four you don't.
How do I check my connector in two minutes?#
The check is a four-pass read against your own instance, and the step nobody does is the one that matters: confirm the credential field is actually type password2 in the dictionary, not merely masked on the form. A masked value on screen tells you the form hides it — it does not tell you the stored value is encrypted2. Run these in order and you'll know in about two minutes whether the secret is safe or recoverable.
- In the filter navigator, open
sys_properties.listand search the Value column for the secret string (or the property name the connector references) — a hit here means the credential is sitting in cleartext, which is the anti-pattern you're hunting. - In Studio or a global search across Script Includes, grep for the client secret literal and for keywords like
client_secret,Basic, andBearer— any hardcoded credential in source is a recoverable secret, no matter how tidy the code looks. - In the REST Message record the connector calls, open the HTTP Method and read the Authorization header line by line — if the secret is baked into the header definition rather than resolved from a credential, it ships inside the message itself.
- On the
oauth_entityrecord, right-click the Client Secret field and open Configure Dictionary (or querysys_dictionaryfor that column) — the Type must read Password (2-Way Encrypted) /password2. If it reads String, the masking on the form is cosmetic and the value is recoverable.
Step four is where I catch the ones the first three miss. A field that shows dots on the form feels safe, so people stop there — but display masking and encrypted storage are two different settings, and an AI that reached for the convenient spot often gets the first without the second. If any pass turns something up — a plaintext property, a hardcoded literal, a String-typed credential field — you've found exactly what a cert reviewer or a client security team would find, just earlier and on your own terms.
Won't our pipeline scanners catch a hardcoded secret?#
Your scanner catches the secret it can see — a literal hardcoded in a Script Include is exactly what SAST is built to flag, and a good one will. But that's only one of the four spots, and it's the honest half of the objection. The other three never touch your pipeline. A secret written to sys_properties or dropped into an oauth_entity row is instance data, not source code — it lives in a database table on the target instance, not in a repo your CI ever scans. Nothing crosses the pipeline for the scanner to inspect.
That's the gap. Pipeline tooling assumes the risky thing is in the code you commit. On ServiceNow, a lot of what the AI does lands as configuration and records instead — and update-set-native change means the "artifact" is a row, not a diff your linter reads. So the plaintext property that failed cert in the rebuild would have sailed straight through a code scanner, because there was no code to scan.
And the volume is going the wrong way. Since Build Agent went GA in May 20267, more of these connectors are generated than anyone reads line by line — while Veracode's Spring 2026 slice shows the exact failure mode: security pass rates sat flat near 55% even as syntax pass climbed past 95%1, a finding an independent read of the same data confirms5. The code looks more right than ever and is no safer. Ship unread connectors for two more quarters and you're not saving time — you're building a remediation queue you'll clear at your own cost.
You found plaintext — now what?#
Move the value into oauth_entity.client_secret and prove it's encrypted before cert review does — don't just delete the plaintext copy and assume you're clean. The fix is a relocation, not a patch: put the secret where the password2 field encrypts it2, wire the connector to read from there, and delete the recoverable copy you found. If you were using an alias, point it at an actually-encrypted field, not the cleartext one it was resolving before4. ServiceNow's own guidance is the bar you're clearing here — no plaintext credentials on the outbound token call3.
Do it in the order that keeps you safe. Read the instance first — you already have, that's how you found the plaintext. Then plan the change in plain terms before you touch anything: the secret moves to the encrypted field, the connector reads from it, the cleartext copy goes. Capture the whole thing in an update set so it's reversible — if the relocation breaks the token call, you back it out cleanly instead of chasing a half-migrated credential across the instance. Get a human eye on the plan before it executes. That's Read → Plan → Approve → Prove, and the last word is the one people skip.
Proving it means a check that fails when the secret isn't encrypted — not a connector that authenticates. Those are different claims, and the whole trap in this piece was mistaking the second for the first. Re-run the two-minute read: sys_properties clean, no hardcoded literal, the credential field typed password2 in the dictionary. Better, write an asserting test that reads the token call succeeds and that the stored value isn't recoverable, so the check turns red the day someone re-introduces a plaintext copy. When we rebuilt that FSM connector, the fix wasn't just relocating the secret — it was replacing the hollow tests with ones that go red when a behavior breaks6. A green suite that never asserted the credential was encrypted is how the plaintext shipped the first time.
Doing this by hand works, and if you've read this far you can. Where it gets slow is scale — every AI-built connector, every instance, every review. XE Community does the first two passes for you: it flags secrets in context — keys, passwords, tokens sitting in fields before they ever egress — and it reads the instance before you change anything, so you're inspecting the actual record state instead of trusting the form. It's free on the Chrome Web Store, runs on your own Anthropic key, and stays read-only by default on dev and sub-prod. Install it, point it at the connector the AI just wired up, and let it tell you where the secret really landed.
References#
- Spring 2026 GenAI Code Security Update~45% of AI code-gen tasks introduce a known security flaw; security ~55% flat while syntax >95%↩
- Password2 encryption with Key Management (Zurich)Backs that client_secret is a Password (2-Way Encrypted) field↩
- Avoid using credentials in plain text when making outbound OAuth token calls (KB0752549)Vendor's own warning against plaintext credential storage↩
- Connections, Credentials, Aliases and OAuthCorrect pattern: connection & credential aliases as the sanctioned home for the secret vs a plaintext property↩
- Veracode: AI-Generated Code Security Has Barely ImprovedIndependent (non-vendor) corroboration of the Veracode Spring 2026 finding↩
- Founder account↩
- ServiceNow Newsroom, May 2026↩