← All KB Checks
CRITICALLinterCheck

eval() Usage Detector

nowisor-eval-usage-detector··Source on GitHub →

eval() Usage Detector

This page describes the nowisor-eval-usage-detector check from the nowisor Instance Scan Pack v1.0.0. The check is a LinterCheck — it walks every server-side script in your instance (Script Includes, Business Rules, UI Actions) and emits one finding per call to eval(). A typical instance produces zero to a few dozen findings; the pattern is rare in OOB code, but every occurrence is high-value because it represents a runtime code-execution sink.

What this finding means

Every finding identifies one line of server-side JavaScript that calls eval(). The check matches the AST shape NAME 'eval' whose parent is a CALL node — that is, a direct function-call invocation of eval. This is the simplest AST shape and the most common form of the call: eval('1+1'), eval(someStringVar), eval(record.getValue('expression')).

eval() executes its string argument as JavaScript at runtime. The platform parses the string into an AST and runs it with the privileges of the surrounding script. When the string is a literal like '1+1', that's surprising but not dangerous. When the string is anything other than a literal — a variable, a record value, a parameter from a Scripted REST API — the line becomes a code-injection sink. Anything an attacker can influence to flow into that string becomes server-side code execution.

The check's expected finding volume is low. eval() is not common in OOB ServiceNow code; the platform's built-in modules use GlideEvaluator instead (which the companion check nowisor-glide-evaluator-detector covers). Custom code is where eval() shows up — typically in legacy integrations, AngularJS controllers ported from older surfaces, or developer-written "configurable expression" patterns. Each finding is therefore worth individual review.

Why it matters for your compliance

eval() is the canonical secure-coding finding. Every modern code-review framework flags it; every regulator's secure-development control extends to it.

NIS2 Article 21§2(d) — supply chain security including security-related aspects concerning the relationships between each entity and its direct suppliers. Article 21§2(d) covers the security expectations entities place on the code shipping into their environments — from internal developers, contractors, or vendor integrations. An eval() call in a third-party integration is a supply-chain artifact that the entity inherited from a supplier relationship. NIS2 expects entities to detect and remediate such artifacts as part of supplier hygiene.

ISO 27001 A.8.28 — secure coding. The 2022 update added A.8.28 specifically to cover secure coding practices. The control text references avoiding dangerous functions and language constructs. eval() is the most cited example in secure-coding curricula across every language family. An ISO auditor asking for evidence of A.8.28 compliance will look at code-review findings related to dynamic evaluation as the front-line evidence.

DORA Article 9 — ICT risk management framework. DORA Article 9§4(b) covers data integrity. Dynamic code evaluation can be coerced into modifying data integrity controls themselves — for instance, an eval() reachable from user input can execute code that alters audit trails. The control therefore extends to detecting and removing dynamic-eval sinks in production code paths.

The check is severity 1 (critical). The reason is asymmetric: the cost of removing an eval() and replacing it with safer alternatives is low (refactor cost), but the cost of leaving it in place is unbounded if any path to user-controlled input exists.

The attack path

The attack path is AP-007 — eval-plus-setWorkflow privilege escalation, documented in the nowisor KB. The pattern is short but devastating.

Step 1 — Find an eval() reachable from user input. The attacker enumerates the script tables (sys_script_include, sys_script, sys_ws_operation) looking for eval() calls. They identify a candidate: a Scripted REST API operation that evaluates an expression string passed in the request body, intended as a "configurable filter" for the integration. The expression string flows directly into eval() without sanitization.

Step 2 — Craft the payload. The attacker constructs a payload that uses the eval context to perform a privileged operation. For instance, to grant themselves an admin role:

var gr = new GlideRecord('sys_user_has_role');
gr.initialize();
gr.user = '<ATTACKER_USER_SYSID>';
gr.role = '<ADMIN_ROLE_SYSID>';
gr.setWorkflow(false);
gr.insert();

The setWorkflow(false) call disables Business Rules and audit logging for the role-grant — pairing the eval-injection with the nowisor-set-workflow-false-detector failure mode to leave no forensic trace.

Step 3 — Inject. The attacker submits the payload as the expression string to the Scripted REST API. The platform parses the request, extracts the expression, calls eval() with it. The role-grant code executes with the privileges of the script's run-as user — typically system, which has full database access regardless of the attacker's ACLs.

Step 4 — Verify and operate. The attacker checks sys_user_has_role for their user. The admin role is granted. They log out, log back in, and now have admin access to the platform. The original Scripted REST API call left a single entry in sys_audit (the API invocation) — but the role-grant itself left nothing because of setWorkflow(false).

The defense is structural: remove the eval(). There is no input validation that reliably constrains a JavaScript string to a safe subset; the language has too many escape paths.

How to fix it

This is a triage check, not a one-shot fix. Each finding needs individual review.

For each finding, classify the call:

  1. Static literal argument (eval('1+1')) — Why was eval() used here at all? The answer is usually "it was a quick way to dispatch on a small set of operations." Refactor to a switch statement or a function map. Remove the eval().

  2. Variable or expression argument from internal state (the variable is constructed from data the script itself controls, with no external input) — Lower risk, but still a smell. Refactor to direct logic. If the variable is genuinely a serialized representation of structured data, replace with JSON.parse().

  3. Variable from user-controlled input (form field, REST parameter, table value editable by users) — Highest priority. This is the AP-007 sink. Remove it immediately. Refactor the surrounding code to use a domain-specific parser or a fixed dispatch table that only accepts known operations.

  4. Legitimate JSON parsing fallback (the script tries JSON.parse() first and falls back to eval() for older JSON-ish strings) — Drop the fallback. Old JSON parsing patterns are not worth the injection sink. If the data really isn't valid JSON, fix the producer.

The platform has no legitimate use case for eval() in customer-authored code. Every finding should resolve to "removed" or "documented exception with explicit narrative of why no input can reach it."

How to verify the fix

Pull the current set of eval() findings using this Background Script:

/*
 * Verify nowisor-eval-usage-detector remediation progress
 * Read-only, safe for production.
 * Lists current open findings grouped by source script.
 */
(function pullEvalFindings() {
    var byScript = {};
    var f = new GlideRecord('scan_finding');
    f.addQuery('check.sys_scope.scope', 'x_nowisor_isp');
    f.addQuery('check.name', 'eval() Usage Detector');
    f.query();

    while (f.next()) {
        var sourceName = f.getDisplayValue('source') || '(unknown)';
        if (!byScript[sourceName]) byScript[sourceName] = 0;
        byScript[sourceName]++;
    }

    var names = Object.keys(byScript).sort();
    gs.print('=== eval() findings by source ===');
    gs.print('Total scripts with findings: ' + names.length);
    gs.print('');
    for (var i = 0; i < names.length; i++) {
        gs.print('  ' + names[i] + ': ' + byScript[names[i]] + ' call(s)');
    }
    if (names.length === 0) {
        gs.print('Clean: no eval() calls detected in current scan.');
    }
})();

Re-run after each remediation to confirm the count drops. Track the trend in your code-review record.

For each removed eval(), the verification is straightforward: open the script record, search for the literal string eval(, confirm zero matches.

What to do next

eval() rarely appears alone. The same code-discipline gaps that allowed it usually allow other dynamic-code patterns and audit-bypass patterns:

For an entity under NIS2 supply-chain scrutiny, the nowisor advisor product is being built to group dynamic-evaluation findings (eval + GlideEvaluator) with their corresponding supplier attribution — which third-party integration introduced the pattern, which internal team owns the affected scope — so your supplier-management process has the artifact it needs. Audit your dynamic-eval surface →