@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
160 lines • 8.07 kB
JavaScript
;
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