UNPKG

@eagleoutice/flowr

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

160 lines 8.07 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PROBLEMATIC_INPUTS = void 0; const linter_format_1 = require("../linter-format"); const flowr_search_builder_1 = require("../../search/flowr-search-builder"); const range_1 = require("../../util/range"); const linter_tags_1 = require("../linter-tags"); const simple_input_classifier_1 = require("../../queries/catalog/input-sources-query/simple-input-classifier"); const parse_1 = require("../../slicing/criterion/parse"); const graph_1 = require("../../dataflow/graph/graph"); const defaultConsider = ['^eval$', '^system$', '^system2$', '^shell$']; const defaultPipeCommandFunctions = [ { pattern: '^pdf$', argIdx: 0, argName: 'file' }, { pattern: '^postscript$', argIdx: 0, argName: 'file' } ]; function normalizePatternList(cfg, defaults) { if (cfg === undefined) { return Array.from(defaults, s => new RegExp(s)); } if (Array.isArray(cfg)) { const arr = cfg.length === 0 ? Array.from(defaults) : cfg; return Array.from(new Set(arr), s => new RegExp(s)); } return [new RegExp(cfg)]; } function normalizePipeSpecs(cfg) { const raw = cfg === undefined ? defaultPipeCommandFunctions : Array.isArray(cfg) ? (cfg.length === 0 ? defaultPipeCommandFunctions : cfg) : [cfg]; return raw.map(s => ({ pattern: new RegExp(s.pattern), argIdx: s.argIdx, argName: s.argName })); } function formatInputSources(inputs, inline = true) { if (!inputs || inputs.length === 0) { return inline ? '' : []; } const fmt = (s) => { const t = '[' + s.types.join(',') + ']'; const c = s.cds ? ', cds: [' + s.cds.join(',') + ']' : ''; return inline ? `${s.id} (type: ${t}, trace: ${s.trace}${c})` : `- ${s.id}: type=${t}, trace=${s.trace}${c}`; }; return inline ? inputs.map(fmt).join('; ') : inputs.map(fmt); } function hasUnknownSource(sources) { return sources.some(s => s.types.includes(simple_input_classifier_1.InputType.Unknown)); } function isProblematicForAllowed(sources, allowed) { return sources.some(s => s.types.some(t => !allowed.includes(t))); } function getPipeCommandValue(sources) { for (const s of sources) { if (typeof s.value === 'string' && s.value.startsWith('|')) { return s.value; } } return undefined; } function resolveFileArgId(vertex, argIdx, argName) { const args = vertex?.args; if (!args) { return undefined; } let idx = args.findIndex(a => graph_1.FunctionArgument.isNamed(a) && a.name === argName); if (idx < 0) { idx = argIdx; } if (idx >= args.length) { return undefined; } const arg = args[idx]; return graph_1.FunctionArgument.isEmpty(arg) ? undefined : graph_1.FunctionArgument.getReference(arg); } function checkPipeInjection(nid, loc, name, sources) { const pipeCmd = getPipeCommandValue(sources); if (pipeCmd !== undefined) { return { involvedId: nid, certainty: linter_format_1.LintingResultCertainty.Certain, loc, name, sources, pipeCommand: pipeCmd }; } if (hasUnknownSource(sources)) { return { involvedId: nid, certainty: linter_format_1.LintingResultCertainty.Uncertain, loc, name, sources }; } return undefined; } exports.PROBLEMATIC_INPUTS = { createSearch: config => { const toQ = (name, subkind) => ({ type: 'call-context', callName: name, callNameExact: false, subkind }); return flowr_search_builder_1.Q.fromQuery(...normalizePatternList(config?.consider, defaultConsider).map((n, i) => toQ(n, `fn-${i}`)), ...normalizePipeSpecs(config?.pipeCommandFunctions).map((s, i) => toQ(s.pattern, `pipe-${i}`))); }, processSearchResult: async (elements, config, data) => { const results = []; const seen = new Set(); const defaultAccept = [simple_input_classifier_1.InputType.Constant, simple_input_classifier_1.InputType.DerivedConstant]; const considerPats = normalizePatternList(config?.consider, defaultConsider); const pipePats = normalizePipeSpecs(config?.pipeCommandFunctions); for (const element of elements.getElements()) { const nid = element.node.info.id; if (seen.has(nid)) { continue; } const name = element.node.lexeme ?? ''; const pipeSpec = pipePats.find(s => s.pattern.test(name)); const isConsider = pipeSpec === undefined && considerPats.some(p => p.test(name)); if (pipeSpec === undefined && !isConsider) { continue; } const loc = range_1.SourceLocation.fromNode(element.node) ?? range_1.SourceLocation.invalid(); if (pipeSpec !== undefined) { const vertex = data.dataflow.graph.getVertex(nid); const fileArgId = resolveFileArgId(vertex, pipeSpec.argIdx, pipeSpec.argName); if (fileArgId !== undefined) { const criterion = parse_1.SlicingCriterion.fromId(fileArgId); const all = await data.analyzer.query([{ type: 'input-sources', criterion, config: config.inputFns }]); const sources = all['input-sources']?.results?.[criterion] ?? []; const r = checkPipeInjection(nid, loc, name, sources); if (r !== undefined) { seen.add(nid); results.push(r); } } } else { const criterion = parse_1.SlicingCriterion.fromId(nid); const all = await data.analyzer.query([{ type: 'input-sources', criterion, config: config.inputFns }]); const sources = all['input-sources']?.results?.[criterion] ?? []; if (isProblematicForAllowed(sources, defaultAccept)) { seen.add(nid); results.push({ involvedId: nid, certainty: hasUnknownSource(sources) ? linter_format_1.LintingResultCertainty.Uncertain : linter_format_1.LintingResultCertainty.Certain, loc, name, sources }); } } } return { results, '.meta': {} }; }, prettyPrint: { [linter_format_1.LintingPrettyPrintContext.Query]: result => { if (result.pipeCommand !== undefined) { return `Pipe command injection via '${result.pipeCommand}' in call to ${result.name} at ${range_1.SourceLocation.format(result.loc)}`; } const src = formatInputSources(result.sources ?? [], true); return 'Use of configured dynamic call at ' + range_1.SourceLocation.format(result.loc) + (src ? '; inputs: ' + src : ''); }, [linter_format_1.LintingPrettyPrintContext.Full]: result => { const lines = formatInputSources(result.sources ?? [], false); const tail = lines.length ? '\nInputs:\n' + lines.join('\n') : ''; return result.pipeCommand !== undefined ? `Pipe command injection: '${result.pipeCommand}' passed as filename to ${result.name} at ${range_1.SourceLocation.format(result.loc)}${tail}` : 'Use of configured dynamic call at ' + range_1.SourceLocation.format(result.loc) + ' is potentially problematic' + tail; } }, info: { name: 'Problematic inputs', description: 'Detects uses of dynamic calls (e.g. eval, system) with non-constant inputs, and graphics-device calls (pdf, postscript) where a filename starts with \'|\' indicating a pipe command injection.', tags: [linter_tags_1.LintingRuleTag.Security, linter_tags_1.LintingRuleTag.Smell, linter_tags_1.LintingRuleTag.Readability, linter_tags_1.LintingRuleTag.Performance], certainty: linter_format_1.LintingRuleCertainty.BestEffort, defaultConfig: { consider: defaultConsider, pipeCommandFunctions: defaultPipeCommandFunctions } } }; //# sourceMappingURL=problematic-inputs.js.map