Skip to content

[Linting Rule] Unescaped Arguments

github-actions[bot] edited this page Sep 1, 2026 · 1 revision

Generated from 'wiki-linter.ts' on 2026-08-26, 19:37:16 UTC (v2.15.3), please do not edit directly.

Unescaped Arguments [overview]

smell security quickfix shiny

This rule is a best-effort rule.

Detects arguments of critical system, evaluation, database, and HTML/JavaScript calls that are not properly escaped.
This linting rule is implemented in src/linter/rules/unescaped-arguments.ts.

Configuration

Linting rules can be configured by passing a configuration object to the linter query as shown in the example below. The unescaped-arguments rule accepts the following configuration options:

  • acceptedInputs
    The input types that count as already escaped
  • categories
    The target, critical functions, critical arguments, sanitizers and quick fixes for each category
  • disabledCategories
    The categories that should be disabled and not checked
  • maxDecentDepth
    The maximum depth to descent to find unescaped parts of an argument

Examples

function(dir) {
	system(paste0("ls ", dir))
}

The linting query can be used to run this rule on the above example:

[ { "type": "linter",   "rules": [ { "name": "unescaped-arguments",     "config": {} } ] } ]

Results (prettified and summarized):

Query: linter (13 ms)
   ╰ Unescaped Arguments (unescaped-arguments):
       ╰ uncertain:
           ╰ Unescaped system argument of system at 2.9-26 (1 quick fix(es) available)
       ╰ Metadata: totalCriticalArguments: 1, totalEscapedArguments: 0, searchTimeMs: 1, processTimeMs: 5
All queries together required ≈13 ms (1ms accuracy, total 14 ms)

Show Detailed Results as Json

The analysis required 13.7 ms (including parsing and normalization and the query) within the generation environment.

In general, the JSON contains the Ids of the nodes in question as they are present in the normalized AST or the dataflow graph of flowR. Please consult the Interface wiki page for more information on how to get those.

{
  "linter": {
    "results": {
      "unescaped-arguments": {
        "results": [
          {
            "certainty": "uncertain",
            "involvedId": [
              12,
              10
            ],
            "loc": [
              2,
              9,
              2,
              26
            ],
            "category": "system",
            "function": "system",
            "sources": [
              {
                "id": 8,
                "types": [
                  "param"
                ],
                "trace": "pure"
              }
            ],
            "input": [
              "param"
            ],
            "quickFix": [
              {
                "type": "replace",
                "loc": [
                  2,
                  23,
                  2,
                  25
                ],
                "description": "Escape the value with `shQuote`",
                "replacement": "shQuote(dir)"
              }
            ]
          }
        ],
        ".meta": {
          "totalCriticalArguments": 1,
          "totalEscapedArguments": 0,
          "searchTimeMs": 1,
          "processTimeMs": 5
        }
      }
    },
    ".meta": {
      "timing": 13
    }
  },
  ".meta": {
    "timing": 13
  }
}

Additional Examples

These examples are synthesized from the test cases in: test/functionality/linter/lint-unescaped-arguments.test.ts

Test Case: constant command

Given the following input:

system("ls")

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: escaped command

Given the following input:

system(shQuote(x))

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: unknown command

Given the following input:

system(x)

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.System,
function:  'system',
loc:       SourceRange.from(1, 8, 1, 8),
sources:   [{ id: 1, trace: InputTraceType.Unknown, types: [InputType.Unknown] }],
input:     [InputType.Unknown],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(1, 8, 1, 8),
	description: 'Escape the value with `shQuote`',
	replacement: 'shQuote(x)'
}]

See here for the test-case implementation.

Test Case: pasted parameter

Given the following input:

f <- function(dir) system(paste0("ls ", dir))

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.System,
function:  'system',
loc:       SourceRange.from(1, 27, 1, 44),
sources:   [{ id: 7, trace: InputTraceType.Pure, types: [InputType.Parameter] }],
input:     [InputType.Parameter],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(1, 41, 1, 43),
	description: 'Escape the value with `shQuote`',
	replacement: 'shQuote(dir)'
}]

See here for the test-case implementation.

Test Case: pasted parameter with a constant and an unknown call

Given the following input:

f <- function(dir) system(paste0("ls ", dir))
f("ls")
f(x)

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.System,
function:  'system',
loc:       SourceRange.from(1, 27, 1, 44),
sources:   [{ id: 7, trace: InputTraceType.Alias, types: [InputType.Constant, InputType.Scope, InputType.Parameter] }],
input:     [InputType.Scope, InputType.Parameter],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(1, 41, 1, 43),
	description: 'Escape the value with `shQuote`',
	replacement: 'shQuote(dir)'
}]

See here for the test-case implementation.

Test Case: pasted escaped parameter

Given the following input:

f <- function(dir) system(paste0("ls ", shQuote(dir)))

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: partly escaped command

Given the following input:

f <- function(a, b) system(paste0("cp ", shQuote(a), " ", b))

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.System,
function:  'system',
loc:       SourceRange.from(1, 28, 1, 60),
sources:   [{ id: 16, trace: InputTraceType.Pure, types: [InputType.Parameter] }],
input:     [InputType.Parameter],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(1, 59, 1, 59),
	description: 'Escape the value with `shQuote`',
	replacement: 'shQuote(b)'
}]

See here for the test-case implementation.

Test Case: user input as command

Given the following input:

shinyServer('system(input$cmd)')

We expect the linter to report the following:

			certainty: LintingResultCertainty.Certain,
category:  UnescapedArgumentCategory.System,
function:  'system',
loc:       SourceRange.from(3, 9, 3, 17),
sources:   [{ id: 15, trace: InputTraceType.Unknown, types: [InputType.User], name: 'cmd' }],
input:     [InputType.User],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(3, 9, 3, 17),
	description: 'Escape the value with `shQuote`',
	replacement: 'shQuote(input$cmd)'
}]

See here for the test-case implementation.

Test Case: unknown arguments

Given the following input:

system2("ls", args = x)

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.System,
function:  'system2',
loc:       SourceRange.from(1, 22, 1, 22),
sources:   [{ id: 4, trace: InputTraceType.Unknown, types: [InputType.Unknown], name: 'args' }],
input:     [InputType.Unknown],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(1, 22, 1, 22),
	description: 'Escape the value with `shQuote`',
	replacement: 'shQuote(x)'
}]

See here for the test-case implementation.

Test Case: redefined function

Given the following input:

system <- function(command) invisible(command)
system(x)

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: constant evaluation

Given the following input:

eval(parse(text = "1+1"))

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: bounded evaluation

Given the following input:

eval(parse(text = match.arg(x, c("a", "b"))))

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: unknown evaluation

Given the following input:

eval(parse(text = x))

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.Eval,
function:  'eval',
loc:       SourceRange.from(1, 6, 1, 20),
sources:   [{ id: 3, trace: InputTraceType.Unknown, types: [InputType.Unknown], name: 'text' }],
input:     [InputType.Unknown]

See here for the test-case implementation.

Test Case: constant statement

Given the following input:

dbGetQuery(con, "SELECT * FROM t")

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: interpolated statement

Given the following input:

shinyServer('dbGetQuery(con, DBI::sqlInterpolate(con, "SELECT * FROM t WHERE x = ?x", x = input$x))')

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: pasted user input

Given the following input:

shinyServer('dbGetQuery(con, paste0("SELECT * FROM t WHERE x = \'", input$x, "\'"))')

We expect the linter to report the following:

				certainty: LintingResultCertainty.Certain,
category:  UnescapedArgumentCategory.Database,
function:  'dbGetQuery',
loc:       SourceRange.from(3, 18, 3, 68),
sources:   [{ id: 20, trace: InputTraceType.Unknown, types: [InputType.User], name: 'x' }],
input:     [InputType.User],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(3, 56, 3, 62),
	description: 'Escape the value with `DBI::dbQuoteLiteral`',
	replacement: 'DBI::dbQuoteLiteral(con, input$x)'
}]

See here for the test-case implementation.

Test Case: statement built elsewhere

Given the following input:

q <- paste0("SELECT * FROM t WHERE x = ", user)
dbGetQuery(con, q)

We expect the linter to report the following:

				certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.Database,
function:  'dbGetQuery',
loc:       SourceRange.from(2, 17, 2, 17),
sources:   [{ id: 11, trace: InputTraceType.Alias, types: [InputType.Constant, InputType.Unknown, InputType.DerivedConstant] }],
input:     [InputType.Unknown]

See here for the test-case implementation.

Test Case: constant value

Given the following input:

shinyServer('HTML("<b>hi</b>")')

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: escaped user input

Given the following input:

shinyServer('HTML(htmltools::htmlEscape(input$name))')

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: pasted user input

Given the following input:

shinyServer('HTML(paste0("<b>", input$name, "</b>"))')

We expect the linter to report the following:

			certainty: LintingResultCertainty.Certain,
category:  UnescapedArgumentCategory.Html,
function:  'HTML',
loc:       SourceRange.from(3, 7, 3, 39),
sources:   [{ id: 18, trace: InputTraceType.Unknown, types: [InputType.User], name: 'name' }],
input:     [InputType.User],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(3, 21, 3, 30),
	description: 'Escape the value with `htmltools::htmlEscape`',
	replacement: 'htmltools::htmlEscape(input$name)'
}]

See here for the test-case implementation.

Test Case: constant code

Given the following input:

shinyServer('shinyjs::runjs("alert(1)")')

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: serialized user input

Given the following input:

shinyServer('shinyjs::runjs(paste0("alert(", jsonlite::toJSON(input$name), ")"))')

We expect the linter to report the following:

* no lints

See here for the test-case implementation.

Test Case: pasted user input

Given the following input:

shinyServer('shinyjs::runjs(paste0("alert(\'", input$name, "\')"))')

We expect the linter to report the following:

				certainty: LintingResultCertainty.Certain,
category:  UnescapedArgumentCategory.JavaScript,
function:  'shinyjs::runjs',
loc:       SourceRange.from(3, 17, 3, 51),
sources:   [{ id: 18, trace: InputTraceType.Unknown, types: [InputType.User], name: 'name' }],
input:     [InputType.User],
quickFix:  [{
	type:        'replace',
	loc:         SourceRange.from(3, 35, 3, 44),
	description: 'Escape the value with `jsonlite::toJSON`',
	replacement: 'jsonlite::toJSON(input$name)'
}]

See here for the test-case implementation.

Test Case: unknown code

Given the following input:

shinyjs::runjs(x)

We expect the linter to report the following:

			certainty: LintingResultCertainty.Uncertain,
category:  UnescapedArgumentCategory.JavaScript,
function:  'shinyjs::runjs',
loc:       SourceRange.from(1, 16, 1, 16),
sources:   [{ id: 1, trace: InputTraceType.Unknown, types: [InputType.Unknown] }],
input:     [InputType.Unknown]

See here for the test-case implementation.

Clone this wiki locally