mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat(taint): add conservative Java source model * fix(taint): preserve Java import provenance * chore: retry CI after network timeout --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
/**
|
|
* Python taint model (#2204) over real Python CFG and import capture output.
|
|
*/
|
|
|
|
import Python from 'tree-sitter-python';
|
|
import type { ParsedImport } from 'gitnexus-shared';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { createPythonCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/python.js';
|
|
import { emitPythonScopeCaptures } from '../../../src/core/ingestion/languages/python/captures.js';
|
|
import { interpretPythonImport } from '../../../src/core/ingestion/languages/python/interpret.js';
|
|
import { PYTHON_TAINT_MODEL } from '../../../src/core/ingestion/taint/python-model.js';
|
|
import type { SourceSinkSanitizerSpec } from '../../../src/core/ingestion/taint/source-sink-config.js';
|
|
import { hasTaintSafeSites } from '../../../src/core/ingestion/taint/site-safety.js';
|
|
import {
|
|
buildTaintImportIndex,
|
|
matchFunctionSites,
|
|
type FunctionSiteMatches,
|
|
type MatchedSinkCall,
|
|
type MatchedSource,
|
|
} from '../../../src/core/ingestion/taint/match.js';
|
|
import { makeCfgHarness } from '../../helpers/cfg-harness.js';
|
|
|
|
const harness = makeCfgHarness(Python, createPythonCfgVisitor(), 'fixture.py');
|
|
|
|
function importsFor(src: string): ParsedImport[] {
|
|
return emitPythonScopeCaptures(src, 'fixture.py')
|
|
.filter((m) => m['@import.statement'] !== undefined)
|
|
.map((m) => interpretPythonImport(m))
|
|
.filter((p): p is ParsedImport => p !== null);
|
|
}
|
|
|
|
function matchesOf(
|
|
code: string,
|
|
fnIndex = 0,
|
|
spec: SourceSinkSanitizerSpec = PYTHON_TAINT_MODEL,
|
|
): FunctionSiteMatches {
|
|
const cfg = harness.cfgOf(code, fnIndex);
|
|
expect(hasTaintSafeSites(cfg)).toBe(true);
|
|
return matchFunctionSites(cfg, spec, buildTaintImportIndex(importsFor(code)));
|
|
}
|
|
|
|
const allSinks = (m: FunctionSiteMatches): MatchedSinkCall[] =>
|
|
m.statements.flatMap((s) => [...s.sinks]);
|
|
const allSources = (m: FunctionSiteMatches): MatchedSource[] =>
|
|
m.statements.flatMap((s) => [...s.sources]);
|
|
|
|
describe('Python taint model (#2204)', () => {
|
|
it('matches Flask/FastAPI-style request member reads as remote-input sources', () => {
|
|
const m = matchesOf(`
|
|
def f(request, req, other):
|
|
a = request.args
|
|
b = req.json
|
|
c = other.args
|
|
`);
|
|
expect(allSources(m).map((s) => s.entry.kind)).toEqual(['remote-input', 'remote-input']);
|
|
expect(m.hasSource).toBe(true);
|
|
});
|
|
|
|
it('resolves named stdlib imports for command sinks', () => {
|
|
const m = matchesOf(`
|
|
from os import system
|
|
|
|
def f(request):
|
|
system(request.args)
|
|
`);
|
|
const sinks = allSinks(m);
|
|
expect(sinks).toHaveLength(1);
|
|
expect(sinks[0].entry.kind).toBe('command-injection');
|
|
expect(sinks[0].entry.name).toBe('system');
|
|
expect(allSources(m)).toHaveLength(1);
|
|
});
|
|
|
|
it('resolves namespace stdlib imports for subprocess sinks', () => {
|
|
const m = matchesOf(`
|
|
import subprocess as sp
|
|
|
|
def f(request):
|
|
sp.run(request.args)
|
|
`);
|
|
expect(allSinks(m).map((s) => s.entry.name)).toEqual(['run']);
|
|
});
|
|
|
|
it('does not dedupe named imports from a same-tail module path', () => {
|
|
const spec: SourceSinkSanitizerSpec = {
|
|
sources: [],
|
|
sinks: [{ name: 'read_string', kind: 'path-traversal', args: [0], module: 'pkg.Files' }],
|
|
sanitizers: [],
|
|
};
|
|
const m = matchesOf(
|
|
`
|
|
from pkg.Files import Files
|
|
|
|
def f(p):
|
|
Files.read_string(p)
|
|
`,
|
|
0,
|
|
spec,
|
|
);
|
|
expect(allSinks(m)).toHaveLength(0);
|
|
});
|
|
|
|
it('does not guess positional sink slots for keyword arguments', () => {
|
|
const m = matchesOf(`
|
|
import subprocess as sp
|
|
|
|
def f(request):
|
|
sp.run(shell=request.args, args="safe")
|
|
`);
|
|
expect(allSinks(m)).toHaveLength(0);
|
|
expect(allSources(m)).toHaveLength(1);
|
|
});
|
|
|
|
it('matches global code and path sinks at argument position zero', () => {
|
|
const m = matchesOf(`
|
|
def f(request):
|
|
eval(request.args)
|
|
exec(request.form)
|
|
open(request.path_params)
|
|
`);
|
|
const sinks = allSinks(m);
|
|
expect(sinks.map((s) => s.entry.kind)).toEqual([
|
|
'code-injection',
|
|
'code-injection',
|
|
'path-traversal',
|
|
]);
|
|
expect(sinks.map((s) => [...s.argPositions])).toEqual([[0], [0], [0]]);
|
|
expect(allSources(m)).toHaveLength(3);
|
|
});
|
|
|
|
it('matches conventional database calls at argument position zero', () => {
|
|
const m = matchesOf(`
|
|
def f(request, cursor, db):
|
|
cursor.execute(request.args)
|
|
db.query(request.form)
|
|
cursor.executemany(request.json)
|
|
cursor.executescript(request.data)
|
|
`);
|
|
const sinks = allSinks(m);
|
|
expect(sinks.map((s) => s.entry.name)).toEqual([
|
|
'execute',
|
|
'query',
|
|
'executemany',
|
|
'executescript',
|
|
]);
|
|
expect(sinks.map((s) => [...s.argPositions])).toEqual([[0], [0], [0], [0]]);
|
|
expect(allSources(m)).toHaveLength(4);
|
|
});
|
|
|
|
it('does not match a locally shadowed stdlib sink name', () => {
|
|
const m = matchesOf(`
|
|
from os import system
|
|
|
|
def f(request):
|
|
def system(value):
|
|
return value
|
|
system(request.args)
|
|
`);
|
|
expect(allSinks(m)).toHaveLength(0);
|
|
expect(allSources(m)).toHaveLength(1);
|
|
});
|
|
|
|
it('does not let methods in a nested class shadow an enclosing import', () => {
|
|
const m = matchesOf(`
|
|
from os import system
|
|
|
|
def f(request):
|
|
class Helpers:
|
|
def system(self, value):
|
|
return value
|
|
system(request.args)
|
|
`);
|
|
expect(allSinks(m)).toHaveLength(1);
|
|
expect(allSources(m)).toHaveLength(1);
|
|
});
|
|
|
|
it('treats a nested class declaration as a local sink-name binding', () => {
|
|
const m = matchesOf(`
|
|
def f(request):
|
|
class open:
|
|
pass
|
|
open(request.args)
|
|
`);
|
|
expect(allSinks(m)).toHaveLength(0);
|
|
expect(allSources(m)).toHaveLength(1);
|
|
});
|
|
});
|