Almost certainly yes. ServiceNow instances accumulate passwords, API keys and tokens across hundreds of fields, because the platform gets used as a credential store despite never having been designed as one. The locations are predictable — work notes and descriptions on change and incident records, script fields, and integration configuration — and the read-only detection script below sweeps them so you can measure the problem before deciding what to rotate.
What This Is
ServiceNow instances accumulate credentials, API keys, tokens, and other secrets across hundreds of tables and fields over time. Unlike purpose-built secrets management tools (HashiCorp Vault, AWS Secrets Manager), ServiceNow was not designed as a credential vault — yet it becomes one through daily operational use.
Secrets end up in ServiceNow through multiple pathways:
How Credentials Enter ServiceNow
| Pathway | Tables Affected | How It Happens |
|---|---|---|
| Change management | change_request, task |
Engineers paste connection strings, passwords, API keys in change descriptions and work notes during implementations |
| Incident management | incident, problem |
Support staff paste error logs containing credentials; users submit incidents with passwords in descriptions |
| Knowledge Base | kb_knowledge |
How-to articles include login procedures with actual credentials; integration guides with API keys |
| Integration configuration | sys_properties, ecc_queue |
API keys stored in system properties; integration payloads in ECC queue containing auth tokens |
| Discovery | discovery_credentials, sa_credential |
Purpose-built credential storage — but access controls may be insufficient |
| Script includes / Business rules | sys_script, sys_script_include |
Hardcoded passwords in server-side scripts; plaintext connection strings |
sys_email |
Auto-generated password reset emails stored in email logs; integration failure emails with credential details | |
| Attachments | sys_attachment |
Config files, .env files, certificate files attached to records |
| Catalog variables | sc_item_option_mtom |
Service catalog forms that collect credentials from users |
| Update sets | sys_update_xml |
Credentials captured in update set XML when properties or scripts are modified |
Scale of the Problem
Security research has documented that ServiceNow instances routinely contain credentials and tokens scattered across multiple tables. When combined with access control weaknesses (Empty Condition ACLs on Sensitive Tables, Knowledge Base Article Access Control Exposure), these stored secrets become extractable by any user with broad read access — or by unauthenticated attackers if public access is misconfigured.
The risk multiplies in clone scenarios (Sub-Production Instance Cloning & Data Exposure Risks): when production is cloned to sub-production, all stored credentials are copied to environments with weaker access controls, broader user access, and less monitoring.
Why This Is Dangerous
Attack Scenario: Credential Harvesting from Change Records
Precondition: Attacker has itil role (standard help desk access) or exploits empty condition ACL on change_request (see Empty Condition ACLs on Sensitive Tables).
Attack chain:
Query change records for credential patterns:
GET /api/now/table/change_request ?sysparm_query=descriptionLIKEpassword ^ORdescriptionLIKEcredential ^ORdescriptionLIKEapi_key ^ORdescriptionLIKEtoken ^ORdescriptionLIKEconnection string &sysparm_fields=number,short_description,description,work_notes &sysparm_limit=500Extract work notes (journal fields) that contain credentials:
GET /api/now/table/sys_journal_field ?sysparm_query=elementINwork_notes,comments ^valueLIKEpassword ^element_idISNOTEMPTY &sysparm_fields=value,element_id,name,sys_created_on &sysparm_limit=1000Search for specific credential formats:
- AWS keys:
AKIA[0-9A-Z]{16} - Azure connection strings:
AccountKey=... - JDBC URLs:
jdbc:...password=... - Bearer tokens:
Bearer eyJ... - SSH keys:
-----BEGIN RSA PRIVATE KEY----- - ServiceNow API tokens:
token=...
- AWS keys:
Harvest and validate: Test extracted credentials against target systems. Credentials in change records are often current — they were pasted during recent implementations.
Impact: A single change record from a database migration may contain the full JDBC connection string with production database credentials. A cloud infrastructure change may contain AWS access keys. A ServiceNow integration change may contain admin-level API tokens.
Attack Scenario: Script-Embedded Credentials
Precondition: Attacker has admin role or read access to sys_script, sys_script_include tables.
Attack chain:
Search all server-side scripts for hardcoded credentials:
// Background script to find credential patterns in scripts var tables = ['sys_script', 'sys_script_include', 'sys_script_client', 'sysauto_script', 'sys_processor', 'sys_ws_operation']; for (var t = 0; t < tables.length; t++) { var gr = new GlideRecord(tables[t]); gr.addQuery('active', true); gr.addEncodedQuery( 'scriptLIKEpassword^ORscriptLIKEapikey^ORscriptLIKEapi_key' + '^ORscriptLIKEsecret^ORscriptLIKEtoken^ORscriptLIKEBearer' ); gr.query(); // Attacker reviews each matching script for actual credentials }Extract credentials from integration scripts: Common patterns:
// BAD: Hardcoded credentials in script include var restMessage = new sn_ws.RESTMessageV2(); restMessage.setEndpoint('https://api.vendor.com/v2/data'); restMessage.setRequestHeader('Authorization', 'Bearer sk-live-abc123def456...'); // BAD: Database password in scheduled job var jdbcUrl = 'jdbc:oracle:thin:admin/P@ssw0rd123@prod-db:1521/ORCL'; // BAD: API key in system property accessed by script var apiKey = gs.getProperty('x_vendor.api_key'); // Property may contain plaintext keyPivot to external systems: Use extracted API keys and passwords to access vendor APIs, cloud platforms, databases, and other integrated systems.
Attack Scenario: Update Set Credential Leakage
Precondition: Access to sys_update_xml table (typically requires admin role, but may be broader in dev/test instances).
Attack chain:
Query update sets for credential-containing records:
GET /api/now/table/sys_update_xml ?sysparm_query=payloadLIKEpassword^ORpayloadLIKEapi_key^ORpayloadLIKEsecret &sysparm_fields=name,type,payload &sysparm_limit=100Extract credentials from XML payloads: When a system property or script containing credentials is modified, the full XML representation (including the credential value) is captured in the update set. Even if the property is later changed, the historical value persists in the update set record.
Cross-reference across instances: Update sets exported from production to sub-production carry credentials. If sub-production has weaker controls, credentials are more accessible.
How to Detect
Comprehensive Credential Scanner
/*
* DATA-006 Credential Scanner
* Scans key tables and journal fields for stored secrets and credential patterns
*
* Run as: Background script with admin role
* Impact: Read-only, may take several minutes on large instances
* Versions: Orlando+
*
* WARNING: This script searches for credential patterns.
* Output may contain actual credentials — handle securely.
*
* NOTE: Results capped at 500 per table/source.
* Rerun with higher limit for full coverage on large instances.
*
* IMPORTANT: work_notes and close_notes are journal fields stored in
* sys_journal_field, not on parent tables. A LIKE query against them
* on change_request/incident will NOT return results. This script
* scans sys_journal_field separately (Part 2).
*/
var SCAN_CONFIG = {
// Tables and fields to scan
// NOTE: Journal fields (work_notes, close_notes, comments) are NOT on
// parent tables — they are stored in sys_journal_field and scanned in Part 2
targets: [
{ table: 'change_request', fields: ['description'], label: 'Change Records' },
{ table: 'incident', fields: ['description'], label: 'Incidents' },
{ table: 'problem', fields: ['description'], label: 'Problems' },
{ table: 'kb_knowledge', fields: ['text', 'short_description'], label: 'KB Articles' },
// Exclude type=password2 — these are already encrypted at rest
{ table: 'sys_properties', fields: ['value'], label: 'System Properties (plaintext only)', excludeType: 'password2' },
{ table: 'sys_script', fields: ['script'], label: 'Business Rules' },
{ table: 'sys_script_include', fields: ['script'], label: 'Script Includes' },
{ table: 'sysauto_script', fields: ['script'], label: 'Scheduled Jobs' },
{ table: 'sys_ws_operation', fields: ['operation_script'], label: 'Web Service Operations' },
{ table: 'sys_rest_message_fn', fields: ['rest_headers', 'rest_endpoint'], label: 'REST Message Functions' },
{ table: 'oauth_entity', fields: ['client_secret'], label: 'OAuth Applications' },
// NOTE: sys_email body may truncate at 255 chars via getValue() in some versions.
// Use gr.getElement('body').getValue() if results seem incomplete.
{ table: 'sys_email', fields: ['body', 'subject'], label: 'Email Records' },
],
// Credential patterns — used for detailed matching on retrieved records
patterns: [
{ name: 'Password field', regex: 'password\\s*[=:]\\s*["\']?[^\\s"\']{4,}' },
{ name: 'API Key', regex: 'api[_-]?key\\s*[=:]\\s*["\']?[A-Za-z0-9_\\-]{16,}' },
{ name: 'Bearer Token', regex: 'Bearer\\s+[A-Za-z0-9_\\-\\.]{20,}' },
{ name: 'AWS Access Key', regex: 'AKIA[0-9A-Z]{16}' },
{ name: 'AWS Secret Key', regex: 'aws_secret_access_key\\s*[=:]' },
{ name: 'Azure Connection', regex: 'AccountKey=[A-Za-z0-9+/=]{20,}' },
{ name: 'JDBC Connection', regex: 'jdbc:[a-z]+:.*password=' },
{ name: 'SSH Private Key', regex: 'BEGIN\\s+(RSA|DSA|EC|OPENSSH)\\s+PRIVATE\\s+KEY' },
{ name: 'Connection String', regex: '(Server|Data Source)=.*(Password|Pwd)=' },
{ name: 'Basic Auth Header', regex: 'Basic\\s+[A-Za-z0-9+/=]{10,}' },
{ name: 'OAuth Client Secret', regex: 'client_secret\\s*[=:]\\s*["\']?[A-Za-z0-9_\\-]{16,}' },
{ name: 'Private Key PEM', regex: 'BEGIN\\s+CERTIFICATE' },
{ name: 'Slack Token', regex: 'xox[bpors]-[0-9a-zA-Z\\-]{10,}' },
{ name: 'Generic Token', regex: 'token\\s*[=:]\\s*["\']?[A-Za-z0-9_\\-\\.]{20,}' },
],
// LIKE terms for initial GlideRecord queries (fast pre-filter)
searchTerms: ['password', 'api_key', 'apikey', 'secret', 'Bearer',
'AKIA', 'AccountKey', 'jdbc:', 'BEGIN RSA', 'client_secret', 'private_key']
};
gs.info('=== DATA-006: CREDENTIAL SCAN RESULTS ===');
gs.info('Scan started: ' + new GlideDateTime().getDisplayValue());
gs.info('');
var totalFindings = 0;
var findingsByTable = {};
// --- Part 1: Scan regular table fields ---
for (var t = 0; t < SCAN_CONFIG.targets.length; t++) {
var target = SCAN_CONFIG.targets[t];
var tableFindings = 0;
// Build OR query across all fields and search terms
var queryParts = [];
for (var f = 0; f < target.fields.length; f++) {
for (var s = 0; s < SCAN_CONFIG.searchTerms.length; s++) {
queryParts.push(target.fields[f] + 'LIKE' + SCAN_CONFIG.searchTerms[s]);
}
}
var gr = new GlideRecord(target.table);
if (!gr.isValid()) continue;
gr.addEncodedQuery(queryParts.join('^OR'));
// Exclude encrypted properties — they are not plaintext risks
if (target.excludeType) {
gr.addQuery('type', '!=', target.excludeType);
}
gr.setLimit(500);
gr.query();
while (gr.next()) {
tableFindings++;
totalFindings++;
// Run regex patterns against field values to identify credential type
var matchedPattern = 'Keyword match';
var matchedField = 'unknown';
for (var mf = 0; mf < target.fields.length; mf++) {
var val = gr.getValue(target.fields[mf]) || '';
for (var p = 0; p < SCAN_CONFIG.patterns.length; p++) {
var re = new RegExp(SCAN_CONFIG.patterns[p].regex, 'i');
if (re.test(val)) {
matchedPattern = SCAN_CONFIG.patterns[p].name;
matchedField = target.fields[mf];
break;
}
}
if (matchedField !== 'unknown') break;
}
gs.info('[' + target.label + '] ' +
(gr.getValue('number') || gr.getValue('name') || gr.getUniqueValue()) +
' | Field: ' + matchedField +
' | Pattern: ' + matchedPattern +
' | Created: ' + gr.getValue('sys_created_on'));
}
if (tableFindings > 0) {
findingsByTable[target.label] = tableFindings;
}
}
// --- Part 2: Scan journal fields (work_notes, close_notes, comments) ---
// Journal entries are stored in sys_journal_field, NOT on the parent table.
// Querying change_request.work_notes with LIKE will NOT return results.
// This is where developers most commonly paste credentials during implementations.
gs.info('');
gs.info('=== JOURNAL FIELD SCAN ===');
var journalTables = 'change_request,incident,problem';
var journalElements = 'work_notes,close_notes,comments';
var journalFindings = 0;
for (var st = 0; st < SCAN_CONFIG.searchTerms.length; st++) {
var journal = new GlideRecord('sys_journal_field');
journal.addQuery('name', 'IN', journalTables);
journal.addQuery('element', 'IN', journalElements);
journal.addQuery('value', 'LIKE', SCAN_CONFIG.searchTerms[st]);
journal.setLimit(500);
journal.query();
while (journal.next()) {
journalFindings++;
totalFindings++;
// Run regex patterns to identify credential type
var jVal = journal.getValue('value') || '';
var jPattern = 'Keyword match';
for (var jp = 0; jp < SCAN_CONFIG.patterns.length; jp++) {
var jre = new RegExp(SCAN_CONFIG.patterns[jp].regex, 'i');
if (jre.test(jVal)) {
jPattern = SCAN_CONFIG.patterns[jp].name;
break;
}
}
gs.info('[Journal] ' + journal.getValue('name') + '.' +
journal.getValue('element') +
' | Record: ' + journal.getValue('element_id') +
' | Pattern: ' + jPattern +
' | Created: ' + journal.getValue('sys_created_on'));
}
}
if (journalFindings > 0) {
findingsByTable['Journal Fields (work_notes/comments)'] = journalFindings;
}
// --- Summary ---
gs.info('');
gs.info('=== SUMMARY ===');
gs.info('Total potential credential findings: ' + totalFindings);
for (var tbl in findingsByTable) {
gs.info(' ' + tbl + ': ' + findingsByTable[tbl]);
}
gs.info('');
gs.info('NOTE: Results capped at 500 per table/source.');
gs.info('Rerun with higher limit for full coverage on large instances.');
gs.info('');
gs.info('CRITICAL: Review each finding manually to confirm actual credentials.');
gs.info('NEXT STEPS:');
gs.info('1. Rotate any confirmed credentials immediately');
gs.info('2. Remove credentials from records (replace with vault references)');
gs.info('3. Implement credential detection business rule (see remediation)');
gs.info('4. Check access controls on tables with credentials (see ACL-001)');
gs.info('5. If KB articles contain credentials, check KB access (see ACL-009)');
Remediation
Step 1: Immediate — Rotate Discovered Credentials
For every credential found by the scanner:
- Identify the target system the credential accesses
- Rotate the credential in the target system
- Update any legitimate references to the new credential (use credential vault, not plaintext)
- Redact or delete the credential from the ServiceNow record
- Check audit logs for unauthorized use of the credential
Step 2: Implement Credential Detection Business Rule
/*
* Business Rule: Detect and alert on credential storage
*
* Table: change_request (also create for incident, problem, kb_knowledge)
* When: Before insert, Before update
* Active: true
*
* This rule scans work notes and descriptions for credential patterns
* and creates a security alert if found.
*/
(function executeRule(current, previous) {
var fieldsToCheck = ['description', 'work_notes', 'close_notes'];
var patterns = [
/password\s*[=:]\s*["']?[^\s"']{4,}/i,
/api[_-]?key\s*[=:]\s*["']?[A-Za-z0-9_\-]{16,}/i,
/Bearer\s+[A-Za-z0-9_\-\.]{20,}/,
/AKIA[0-9A-Z]{16}/,
/AccountKey=[A-Za-z0-9+\/=]{20,}/,
/jdbc:[a-z]+:.*password=/i,
/BEGIN\s+(RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY/,
/client_secret\s*[=:]\s*["']?[A-Za-z0-9_\-]{16,}/i
];
for (var f = 0; f < fieldsToCheck.length; f++) {
var fieldName = fieldsToCheck[f];
var value = current.getValue(fieldName) || '';
for (var p = 0; p < patterns.length; p++) {
if (patterns[p].test(value)) {
// Create security alert
var alert = new GlideRecord('incident');
alert.initialize();
alert.setValue('short_description',
'[SECURITY] Potential credential detected in ' +
current.getTableName() + ': ' + current.getValue('number'));
alert.setValue('description',
'A credential pattern was detected in the "' + fieldName +
'" field of ' + current.getTableName() + ' ' +
current.getValue('number') + '.\n\n' +
'Please review and remove the credential.\n' +
'Use ServiceNow Credential Vault or an external secrets manager instead.\n\n' +
'Reference: DATA-006 playbook');
alert.setValue('category', 'security');
alert.setValue('priority', 2);
alert.setValue('assignment_group', 'Security Operations');
alert.insert();
// Log the detection (without the actual credential)
gs.info('[DATA-006] Credential pattern detected in ' +
current.getTableName() + ' ' + current.getValue('number') +
' field: ' + fieldName);
break; // One alert per record is sufficient
}
}
}
})(current, previous);
Step 3: Migrate to Proper Credential Storage
ServiceNow provides three purpose-built credential storage mechanisms:
1. Credential Store (discovery_credentials table)
For Discovery, Orchestration, and Service Mapping credentials. Encrypted at rest with instance-level keys. Configure via: Connections & Credentials > Credentials. Ensure ACLs restrict access to security_admin — the default ACL may be too permissive.
2. Connection & Credential Aliases For REST/SOAP integrations (Flow Designer, IntegrationHub). Store credentials as named aliases that scripts reference indirectly — the credential value never appears in code. Configure via: Connections & Credentials > Connection & Credential Aliases.
3. External Credential Resolver (ECR) For MID Server integrations that must retrieve secrets from an external vault (HashiCorp Vault, CyberArk, AWS Secrets Manager). The MID Server calls the external vault at runtime — credentials never enter ServiceNow. Configure via: MID Server > Properties > External Credential Storage.
// GOOD: Use Connection Alias instead of hardcoded credential
var restMessage = new sn_ws.RESTMessageV2('Vendor API', 'POST');
// Credential is resolved from the alias — not in code
restMessage.execute();
// GOOD: Use gs.getProperty() with encrypted property (type: password2)
var apiKey = gs.getProperty('x_vendor.secure_api_key');
// Property is encrypted at rest, not readable via Table API
For system properties containing credentials:
- Change property type from
stringtopassword2(encrypts at rest) - Restrict READ ACL on
sys_propertiestosecurity_adminrole - Never reference encrypted properties in client-callable Script Includes
Step 4: Implement Data Loss Prevention for Scripts
/*
* Script scan — detect hardcoded credentials in new/modified scripts
*
* Business Rule on: sys_script, sys_script_include, sysauto_script
* When: Before insert, Before update
*/
(function executeRule(current, previous) {
var script = current.getValue('script') || '';
var dangerPatterns = [
{ pattern: /['"][A-Za-z0-9_\-]{20,}['"]/g, name: 'Long string literal (possible key)' },
{ pattern: /password\s*=\s*['"][^'"]+['"]/gi, name: 'Hardcoded password' },
{ pattern: /Bearer\s+[A-Za-z0-9_\-\.]{20,}/g, name: 'Bearer token' },
{ pattern: /AKIA[0-9A-Z]{16}/g, name: 'AWS access key' },
];
for (var p = 0; p < dangerPatterns.length; p++) {
if (dangerPatterns[p].pattern.test(script)) {
gs.addErrorMessage(
'SECURITY WARNING: This script appears to contain a hardcoded credential (' +
dangerPatterns[p].name + '). Use gs.getProperty() with a secure system property ' +
'or the Credential Vault instead. Reference: DATA-006.');
// Don't block — warn. Log for security team review.
gs.info('[DATA-006] Credential pattern "' + dangerPatterns[p].name +
'" detected in ' + current.getTableName() + ': ' + current.getValue('name'));
}
}
})(current, previous);
Regulatory Impact
NIS2 Mapping
| Article | Requirement | How Stored Credentials Violate It | Evidence After Fix |
|---|---|---|---|
| Art.21§2(a) | Risk analysis and IS security policies | Unmanaged credentials create uncontrolled access paths to critical systems | Credential inventory completed, vault migration documented, detection rules active |
| Art.21§2(h) | Cryptography and encryption policies | Credentials stored in plaintext in database records violate encryption policy requirements | All credentials migrated to encrypted vault; plaintext credential detection active |
DORA Mapping
| Article | Requirement | How Stored Credentials Violate It | Evidence After Fix |
|---|---|---|---|
| Art.9§4(c) | Mechanisms to detect anomalous activities | Scattered credentials prevent proper access monitoring — can't audit what you don't know exists | Credential inventory enables access monitoring; detection rules prevent new storage |
| Art.9§4(d) | ICT operations security | Credentials in operational records create unmonitored access paths | All integration credentials managed via vault with rotation and audit logging |
ISO 27001:2022 Mapping
| Control | Requirement | How Stored Credentials Violate It | Evidence After Fix |
|---|---|---|---|
| A.8.24 | Use of cryptography | Plaintext credentials in database records | Encrypted credential vault in use; no plaintext credentials in operational records |
| A.5.17 | Authentication information | Authentication secrets not protected during storage | Centralized credential management with access controls and rotation |
Expert Notes
Practitioner annotations pending — article content has been technically validated.