Skip to content

Commit f58571e

Browse files
committed
Merge branch 'fix/sast-findings-dcf712' into 'main'
fix: remediate SAST findings (CWE-78, CWE-918, CWE-770, CWE-489, CWE-185) See merge request postgres-ai/postgresai!228
2 parents 6a1de70 + 990d865 commit f58571e

File tree

6 files changed

+34
-34
lines changed

6 files changed

+34
-34
lines changed

cli/bin/postgres-ai.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,6 @@ function closeReadline() {
5555
}
5656

5757
// Helper functions for spawning processes - use Node.js child_process for compatibility
58-
async function execPromise(command: string): Promise<{ stdout: string; stderr: string }> {
59-
return new Promise((resolve, reject) => {
60-
childProcess.exec(command, (error, stdout, stderr) => {
61-
if (error) {
62-
const err = error as Error & { code: number };
63-
err.code = typeof error.code === "number" ? error.code : 1;
64-
reject(err);
65-
} else {
66-
resolve({ stdout, stderr });
67-
}
68-
});
69-
});
70-
}
71-
7258
async function execFilePromise(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
7359
return new Promise((resolve, reject) => {
7460
childProcess.execFile(file, args, (error, stdout, stderr) => {
@@ -2594,8 +2580,8 @@ mon
25942580

25952581
if (!grafanaPassword) {
25962582
console.log("Generating secure Grafana password...");
2597-
const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
2598-
grafanaPassword = password.trim();
2583+
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
2584+
grafanaPassword = password.trim().replace(/\n/g, "");
25992585

26002586
let configContent = "";
26012587
if (fs.existsSync(cfgPath)) {
@@ -2634,8 +2620,8 @@ mon
26342620
console.log("Generating VictoriaMetrics auth credentials...");
26352621
vmAuthUsername = vmAuthUsername || "vmauth";
26362622
if (!vmAuthPassword) {
2637-
const { stdout: vmPass } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
2638-
vmAuthPassword = vmPass.trim();
2623+
const { stdout: vmPass } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
2624+
vmAuthPassword = vmPass.trim().replace(/\n/g, "");
26392625
}
26402626

26412627
// Update .env file with VM auth credentials
@@ -2947,16 +2933,16 @@ mon
29472933

29482934
// Fetch latest changes
29492935
console.log("Fetching latest changes...");
2950-
await execPromise("git fetch origin");
2936+
await execFilePromise("git", ["fetch", "origin"]);
29512937

29522938
// Check current branch
2953-
const { stdout: branch } = await execPromise("git rev-parse --abbrev-ref HEAD");
2939+
const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
29542940
const currentBranch = branch.trim();
29552941
console.log(`Current branch: ${currentBranch}`);
29562942

29572943
// Pull latest changes
29582944
console.log("Pulling latest changes...");
2959-
const { stdout: pullOut } = await execPromise("git pull origin " + currentBranch);
2945+
const { stdout: pullOut } = await execFilePromise("git", ["pull", "origin", currentBranch]);
29602946
console.log(pullOut);
29612947

29622948
// Update Docker images
@@ -3214,7 +3200,8 @@ targets
32143200
// If YAML parsing fails, fall back to simple check
32153201
const isFile = fs.existsSync(file) && !fs.lstatSync(file).isDirectory();
32163202
const content = isFile ? fs.readFileSync(file, "utf8") : "";
3217-
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
3203+
const escapedName = instanceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3204+
if (new RegExp(`^- name: ${escapedName}$`, "m").test(content)) {
32183205
console.error(`Monitoring target '${instanceName}' already exists`);
32193206
process.exitCode = 1;
32203207
return;
@@ -3658,10 +3645,10 @@ mon
36583645

36593646
try {
36603647
// Generate secure password using openssl
3661-
const { stdout: password } = await execPromise(
3662-
"openssl rand -base64 12 | tr -d '\n'"
3648+
const { stdout: password } = await execFilePromise(
3649+
"openssl", ["rand", "-base64", "12"]
36633650
);
3664-
const newPassword = password.trim();
3651+
const newPassword = password.trim().replace(/\n/g, "");
36653652

36663653
if (!newPassword) {
36673654
console.error("Failed to generate password");
@@ -4712,7 +4699,7 @@ mcp
47124699
// Get the path to the current pgai executable
47134700
let pgaiPath: string;
47144701
try {
4715-
const execPath = await execPromise("which pgai");
4702+
const execPath = await execFilePromise("which", ["pgai"]);
47164703
pgaiPath = execPath.stdout.trim();
47174704
} catch {
47184705
// Fallback to just "pgai" if which fails
@@ -4724,8 +4711,8 @@ mcp
47244711
console.log("Installing PostgresAI MCP server for Claude Code...");
47254712

47264713
try {
4727-
const { stdout, stderr } = await execPromise(
4728-
`claude mcp add -s user postgresai ${pgaiPath} mcp start`
4714+
const { stdout, stderr } = await execFilePromise(
4715+
"claude", ["mcp", "add", "-s", "user", "postgresai", pgaiPath, "mcp", "start"]
47294716
);
47304717

47314718
if (stdout) console.log(stdout);

cli/lib/supabase.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ export async function fetchPoolerDatabaseUrl(
350350
config: SupabaseConfig,
351351
username: string
352352
): Promise<string | null> {
353+
// Validate projectRef format to prevent SSRF via crafted project references
354+
if (!isValidProjectRef(config.projectRef)) {
355+
throw new Error(`Invalid Supabase project reference format: "${config.projectRef}". Expected 10-30 alphanumeric characters.`);
356+
}
353357
const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config.projectRef)}/config/database/pooler`;
354358

355359
// For Supabase pooler connections, the username must include the project ref:

cli/scripts/embed-checkup-dictionary.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,16 @@ function generateTypeScript(data: CheckupDictionaryEntry[], sourceUrl: string):
5151
return lines.join("\n");
5252
}
5353

54+
// Allowed hosts for fetch requests to prevent SSRF
55+
const ALLOWED_HOSTS = ["postgres.ai"];
56+
5457
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
58+
// Validate URL against allowlist to prevent SSRF
59+
const parsed = new URL(url);
60+
if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
61+
throw new Error(`Fetch blocked: host "${parsed.hostname}" is not in the allowlist`);
62+
}
63+
5564
const controller = new AbortController();
5665
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
5766

monitoring_flask_backend/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,4 +1394,4 @@ def get_query_info_metrics():
13941394

13951395

13961396
if __name__ == '__main__':
1397-
app.run(host='0.0.0.0', port=5000, debug=True)
1397+
app.run(host='0.0.0.0', port=5000, debug=False)

reporter/postgres_reports.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def query_instant(self, query: str) -> Dict[str, Any]:
368368
params = {'query': query}
369369

370370
try:
371-
response = requests.get(f"{self.base_url}/query", params=params, auth=self.auth)
371+
response = requests.get(f"{self.base_url}/query", params=params, auth=self.auth, timeout=30)
372372
if response.status_code == 200:
373373
return response.json()
374374
else:
@@ -3115,7 +3115,7 @@ def query_range(self, query: str, start_time: datetime, end_time: datetime, step
31153115
}
31163116

31173117
try:
3118-
response = requests.get(f"{self.base_url}/query_range", params=params, auth=self.auth)
3118+
response = requests.get(f"{self.base_url}/query_range", params=params, auth=self.auth, timeout=30)
31193119
if response.status_code == 200:
31203120
result = response.json()
31213121
if result.get('status') == 'success':
@@ -4890,7 +4890,7 @@ def upload_report_file(self, api_url, token, report_id, path):
48904890

48914891

48924892
def make_request(api_url, endpoint, request_data):
4893-
response = requests.post(api_url + endpoint, json=request_data)
4893+
response = requests.post(api_url + endpoint, json=request_data, timeout=30)
48944894
response.raise_for_status()
48954895
return response.json()
48964896

tests/reporter/test_generators_unit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,7 @@ def raise_for_status(self):
13601360
def json(self):
13611361
return {}
13621362

1363-
def fake_post(url: str, json: dict[str, Any] | None = None):
1363+
def fake_post(url: str, json: dict[str, Any] | None = None, **kwargs):
13641364
return MockResponse()
13651365

13661366
monkeypatch.setattr("requests.post", fake_post)
@@ -1375,7 +1375,7 @@ def test_make_request_raises_on_connection_error(monkeypatch: pytest.MonkeyPatch
13751375
"""Test that make_request raises exception on connection error."""
13761376
import requests
13771377

1378-
def fake_post(url: str, json: dict[str, Any] | None = None):
1378+
def fake_post(url: str, json: dict[str, Any] | None = None, **kwargs):
13791379
raise requests.ConnectionError("Connection failed")
13801380

13811381
monkeypatch.setattr("requests.post", fake_post)

0 commit comments

Comments
 (0)