jashkenas/underscore · error · Error

variable is not a bare identifier:

Error message

variable is not a bare identifier: 

What it means

When you compile an Underscore template with a {variable: 'name'} setting, the name is interpolated verbatim into generated JS, so it must be a single bare identifier (e.g. 'data'), not a dotted path ('a.b') or an expression ('data; process.exit(1)'). The bareIdentifier test guards against code injection via the variable name (CVE-2021-23358), and throws an Error when it fails.

Source

Thrown at modules/template.js:72

    index = offset + match.length;

    if (escape) {
      source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
    } else if (interpolate) {
      source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
    } else if (evaluate) {
      source += "';\n" + evaluate + "\n__p+='";
    }

    // Adobe VMs need the match returned to produce the correct offset.
    return match;
  });
  source += "';\n";

  var argument = settings.variable;
  if (argument) {
    // Insure against third-party code injection. (CVE-2021-23358)
    if (!bareIdentifier.test(argument)) throw new Error(
      'variable is not a bare identifier: ' + argument
    );
  } else {
    // If a variable is not specified, place data values in local scope.
    source = 'with(obj||{}){\n' + source + '}\n';
    argument = 'obj';
  }

  source = "var __t,__p='',__j=Array.prototype.join," +
    "print=function(){__p+=__j.call(arguments,'');};\n" +
    source + 'return __p;\n';

  var render;
  try {
    render = new Function(argument, '_', source);
  } catch (e) {
    e.source = source;
    throw e;

View on GitHub (pinned to e70d5bd070)

Solutions

  1. Use a single valid JavaScript identifier for the variable setting, e.g. _.template(t, {variable: 'data'}).
  2. If you need a nested path, pre-resolve it yourself: pass {variable: 'data'} and read data.prop inside the template, or compute the object before compiling.
  3. Sanitize or validate the variable name with /^[A-Za-z_$][A-Za-z0-9_$]*$/ before passing it, especially when it comes from user input.
  4. Never interpolate user input into the variable setting; treat it as code, not data.
  5. For trusted static templates, hardcode the variable name in source rather than deriving it from runtime config.

Example fix

// before
_.template(html, {variable: 'ctx.model'});
// Error: variable is not a bare identifier: ctx.model

// after
const t = _.template(html, {variable: 'ctx'});
t({ model: ctx.model });
Defensive patterns

Strategy: validation

Validate before calling

const BARE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
function compileTemplate(text, settings = {}) {
  if (settings.variable && !BARE_IDENTIFIER.test(settings.variable)) {
    throw new Error('variable must be a bare identifier: ' + settings.variable);
  }
  return _.template(text, settings);
}

Type guard

const isSafeTemplateVariable = (v) =>
  typeof v === 'string' && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(v);

Try / catch

try {
  return _.template(source, { variable: name });
} catch (e) {
  if (e instanceof Error && e.message.startsWith('variable is not a bare identifier')) {
    return _.template(source, { variable: 'data' }); // safe default
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling _.template(text, {variable: 'some.path'}) or {variable: 'data, other'} — any variable string that is not a plain [A-Za-z_$][\w$]* identifier, including names containing dots, brackets, dashes, spaces, or semicolons (typically from untrusted input).

Common situations: Passing a config-derived or user-supplied variable name (e.g. a dotted property path) straight into the template settings; copying the {variable:'data'} idiom but substituting 'obj.prop' hoping to scope the data; an attacker-controlled variable name triggering the CVE-2021-23358 injection guard.


AI-assisted analysis of jashkenas/underscore@e70d5bd070 (2026-08-29). Data as JSON: /api/errors/eccc9a9158cb7a37. Report an issue: GitHub.