/ utils / powershell / dangerousCmdlets.ts
dangerousCmdlets.ts
  1  /**
  2   * Shared constants for PowerShell cmdlets that execute arbitrary code.
  3   *
  4   * These lists are consumed by both the permission-engine validators
  5   * (powershellSecurity.ts) and the UI suggestion gate (staticPrefix.ts).
  6   * Keeping them here avoids duplicating the lists and prevents sync drift
  7   * — add a cmdlet once, both consumers pick it up.
  8   */
  9  
 10  import { CROSS_PLATFORM_CODE_EXEC } from '../permissions/dangerousPatterns.js'
 11  import { COMMON_ALIASES } from './parser.js'
 12  
 13  /**
 14   * Cmdlets that accept a -FilePath (or positional path) and execute the
 15   * file's contents as a script.
 16   */
 17  export const FILEPATH_EXECUTION_CMDLETS = new Set([
 18    'invoke-command',
 19    'start-job',
 20    'start-threadjob',
 21    'register-scheduledjob',
 22  ])
 23  
 24  /**
 25   * Cmdlets where a scriptblock argument executes arbitrary code (not just
 26   * filtering/transforming pipeline input like Where-Object).
 27   */
 28  export const DANGEROUS_SCRIPT_BLOCK_CMDLETS = new Set([
 29    'invoke-command',
 30    'invoke-expression',
 31    'start-job',
 32    'start-threadjob',
 33    'register-scheduledjob',
 34    'register-engineevent',
 35    'register-objectevent',
 36    'register-wmievent',
 37    'new-pssession',
 38    'enter-pssession',
 39  ])
 40  
 41  /**
 42   * Cmdlets that load and execute module/script code. `.psm1` files run
 43   * their top-level body on import — same code-execution risk as iex.
 44   */
 45  export const MODULE_LOADING_CMDLETS = new Set([
 46    'import-module',
 47    'ipmo',
 48    'install-module',
 49    'save-module',
 50    'update-module',
 51    'install-script',
 52    'save-script',
 53  ])
 54  
 55  /**
 56   * Shells and process spawners. Small, stable — add here only for cmdlets
 57   * not covered by the validator lists above.
 58   */
 59  const SHELLS_AND_SPAWNERS = [
 60    'pwsh',
 61    'powershell',
 62    'cmd',
 63    'bash',
 64    'wsl',
 65    'sh',
 66    'start-process',
 67    'start',
 68    'add-type',
 69    'new-object',
 70  ] as const
 71  
 72  function aliasesOf(targets: ReadonlySet<string>): string[] {
 73    return Object.entries(COMMON_ALIASES)
 74      .filter(([, target]) => targets.has(target.toLowerCase()))
 75      .map(([alias]) => alias)
 76  }
 77  
 78  /**
 79   * Network cmdlets — wildcard rules for these enable exfil/download without
 80   * prompt. No legitimate narrow prefix exists.
 81   */
 82  export const NETWORK_CMDLETS = new Set([
 83    'invoke-webrequest',
 84    'invoke-restmethod',
 85  ])
 86  
 87  /**
 88   * Alias/variable mutation cmdlets — Set-Alias rebinds command resolution,
 89   * Set-Variable can poison $PSDefaultParameterValues. checkRuntimeStateManipulation
 90   * validator in powershellSecurity.ts independently gates on the permission path.
 91   */
 92  export const ALIAS_HIJACK_CMDLETS = new Set([
 93    'set-alias',
 94    'sal', // alias not in COMMON_ALIASES — list explicitly
 95    'new-alias',
 96    'nal', // alias not in COMMON_ALIASES — list explicitly
 97    'set-variable',
 98    'sv', // alias not in COMMON_ALIASES — list explicitly
 99    'new-variable',
100    'nv', // alias not in COMMON_ALIASES — list explicitly
101  ])
102  
103  /**
104   * WMI/CIM process spawn — Invoke-WmiMethod -Class Win32_Process -Name Create
105   * is a Start-Process equivalent that bypasses checkStartProcess. No legitimate
106   * narrow prefix exists; any invocation can spawn arbitrary processes.
107   * checkWmiProcessSpawn validator gates on the permission path.
108   * (security finding #34)
109   */
110  export const WMI_CIM_CMDLETS = new Set([
111    'invoke-wmimethod',
112    'iwmi', // alias not in COMMON_ALIASES — list explicitly
113    'invoke-cimmethod',
114  ])
115  
116  /**
117   * Cmdlets in CMDLET_ALLOWLIST with additionalCommandIsDangerousCallback.
118   *
119   * The allowlist auto-allows these for safe args (StringConstant identifiers).
120   * The permission dialog only fires when the callback rejected — i.e. the args
121   * contain a scriptblock, variable, subexpression, etc. Accepting a
122   * `Cmdlet:*` wildcard at that point would match ALL future invocations via
123   * prefix-startsWith, bypassing the callback forever.
124   * `ForEach-Object:*` → `ForEach-Object { Remove-Item -Recurse / }` auto-allows.
125   *
126   * Sync with readOnlyValidation.ts — test/utils/powershell/dangerousCmdlets.test.ts
127   * asserts this set covers every additionalCommandIsDangerousCallback entry.
128   */
129  export const ARG_GATED_CMDLETS = new Set([
130    'select-object',
131    'sort-object',
132    'group-object',
133    'where-object',
134    'measure-object',
135    'write-output',
136    'write-host',
137    'start-sleep',
138    'format-table',
139    'format-list',
140    'format-wide',
141    'format-custom',
142    'out-string',
143    'out-host',
144    // Native executables with callback-gated args (e.g. ipconfig /flushdns
145    // is rejected, ipconfig /all is allowed). Same bypass risk.
146    'ipconfig',
147    'hostname',
148    'route',
149  ])
150  
151  /**
152   * Commands to never suggest as a wildcard prefix in the permission dialog.
153   *
154   * Derived from the validator lists above plus the small static shells list.
155   * Add a cmdlet to the appropriate validator list and it automatically
156   * appears here — no separate maintenance.
157   */
158  export const NEVER_SUGGEST: ReadonlySet<string> = (() => {
159    const core = new Set<string>([
160      ...SHELLS_AND_SPAWNERS,
161      ...FILEPATH_EXECUTION_CMDLETS,
162      ...DANGEROUS_SCRIPT_BLOCK_CMDLETS,
163      ...MODULE_LOADING_CMDLETS,
164      ...NETWORK_CMDLETS,
165      ...ALIAS_HIJACK_CMDLETS,
166      ...WMI_CIM_CMDLETS,
167      ...ARG_GATED_CMDLETS,
168      // ForEach-Object's -MemberName (positional: `% Delete`) resolves against
169      // the runtime pipeline object — `Get-ChildItem | % Delete` invokes
170      // FileInfo.Delete(). StaticParameterBinder identifies the
171      // PropertyAndMethodSet parameter set, but the set handles both; the arg
172      // is a plain StringConstantExpressionAst with no property/method signal.
173      // Pipeline type inference (upstream OutputType → GetMember) misses ETS
174      // AliasProperty members and has no answer for `$var | %` or external
175      // upstream. Not in ARG_GATED (no allowlist entry to sync with).
176      'foreach-object',
177      // Interpreters/runners — `node script.js` stops at the file arg and
178      // suggests bare `node:*`, auto-allowing arbitrary code via -e/-p. The
179      // auto-mode classifier strips these rules (isDangerousPowerShellPermission)
180      // but the suggestion gate didn't. Multi-word entries ('npm run') are
181      // filtered out — NEVER_SUGGEST is a single-name lookup on cmd.name.
182      ...CROSS_PLATFORM_CODE_EXEC.filter(p => !p.includes(' ')),
183    ])
184    return new Set([...core, ...aliasesOf(core)])
185  })()