Skip to content

Automated Template Builds, Proxmox User ACLs, Improved LDAP Integration#189

Draft
runleveldev wants to merge 17 commits intomainfrom
sprint
Draft

Automated Template Builds, Proxmox User ACLs, Improved LDAP Integration#189
runleveldev wants to merge 17 commits intomainfrom
sprint

Conversation

@runleveldev
Copy link
Collaborator

No description provided.

@runleveldev runleveldev changed the title LDAP Improvements Automated Template Builds, Proxmox User ACLs, Improved LDAP Integration Feb 10, 2026
@runleveldev runleveldev force-pushed the sprint branch 2 times, most recently from 009f4cb to edee54e Compare February 11, 2026 15:13
Comment on lines +26 to +45
const req = https.get(url, { headers }, (res) => {
// Handle redirects
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
const location = res.headers.location;
if (!location) {
return reject(new Error(`Redirect without Location header (status ${res.statusCode})`));
}
// Follow redirect (without auth headers for CDN)
return httpGet(location, {}, redirectCount + 1)
.then(resolve)
.catch(reject);
}

let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (timedOut) return;
resolve({ statusCode: res.statusCode, headers: res.headers, body: data });
});
});

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

In general, to fix SSRF you must prevent untrusted input from freely controlling the destination of outbound HTTP(S) requests. For this code, that means: (1) validating and constraining the registry hostname derived from the image parameter, and (2) optionally tightening redirect following so we don’t escape those constraints via Location headers. The intent here is to fetch metadata from Docker/OCI registries; we can keep that functionality by enforcing that only safe registry hosts are allowed (e.g., public registries or a configured set) and by rejecting image references that specify arbitrary hosts.

The single best fix with minimal behavioral change is to introduce a small validator in docker-registry.js that checks a registry hostname against an allow‑list / safety rules and to apply it before using registry/registryHost to build URLs. Specifically:

  • Add a helper isRegistryHostAllowed(host) that:
    • Ensures the host is a syntactically valid hostname or IPv4/IPv6 literal.
    • Rejects hostnames that resolve to private/bogon IP ranges (RFC1918, loopback, link‑local, etc.) to prevent access to internal networks.
    • Optionally, you can tighten further by allowing only a configured set like docker.io, registry-1.docker.io, ghcr.io, etc. (I’ll implement a conservative allow‑list plus internal-IP blocking).
  • Use Node’s built-in dns.promises.lookup / net module to check resolved IPs, since we are allowed to import standard libraries.
  • In getImageConfig(registry, repo, tag), before constructing registryHost and URLs, call isRegistryHostAllowed(registryHost) and throw an error (or return an HTTP 400 via the caller) if it fails. This is the main path used by GET /metadata.
  • Optionally, add a similar check to any other function that constructs registry URLs with potentially user-controlled hostnames, but within the given snippets only getImageConfig is directly in the flow.

Because getImageConfig is already async, we can comfortably make the validator async and await it. We’ll add the required imports (dns and net) at the top of docker-registry.js and keep everything else intact. This change preserves existing behavior for normal public registries, but prevents users from forcing the backend to call arbitrary/internal hosts.


Suggested changeset 1
create-a-container/utils/docker-registry.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/create-a-container/utils/docker-registry.js b/create-a-container/utils/docker-registry.js
--- a/create-a-container/utils/docker-registry.js
+++ b/create-a-container/utils/docker-registry.js
@@ -7,8 +7,87 @@
  */
 
 const https = require('https');
+const dns = require('dns').promises;
+const net = require('net');
 
 /**
+ * Determine if an IP address is private or loopback/link-local.
+ * This is used to prevent SSRF to internal network resources.
+ * @param {string} ip
+ * @returns {boolean}
+ */
+function isPrivateIp(ip) {
+  if (!ip || typeof ip !== 'string') return false;
+
+  // IPv6 loopback or link-local/site-local
+  if (ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc00:') || ip.startsWith('fd00:')) {
+    return true;
+  }
+
+  const parts = ip.split('.');
+  if (parts.length !== 4) {
+    return false;
+  }
+  const [a, b] = parts.map(n => parseInt(n, 10));
+
+  if (a === 10) return true;                 // 10.0.0.0/8
+  if (a === 127) return true;                // 127.0.0.0/8
+  if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
+  if (a === 192 && b === 168) return true;   // 192.168.0.0/16
+  if (a === 169 && b === 254) return true;   // 169.254.0.0/16 (link-local)
+
+  return false;
+}
+
+/**
+ * Validate that a registry hostname is safe to contact.
+ * Rejects internal/loopback IPs and hostnames that resolve to them.
+ * @param {string} host
+ * @returns {Promise<void>} Resolves if allowed, rejects otherwise.
+ */
+async function isRegistryHostAllowed(host) {
+  if (!host || typeof host !== 'string') {
+    throw new Error('Invalid registry host');
+  }
+
+  // Basic hostname / IPv4 / IPv6 literal check
+  if (!net.isIP(host)) {
+    const hostnameRegex = /^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*$/;
+    if (!hostnameRegex.test(host)) {
+      throw new Error('Disallowed registry host');
+    }
+  }
+
+  // Optional allow-list for known public registries; extend as needed.
+  const allowedHosts = new Set([
+    'docker.io',
+    'registry-1.docker.io',
+    'ghcr.io',
+    'quay.io'
+  ]);
+
+  // Allow explicit entries in the public allow-list without DNS checks.
+  if (allowedHosts.has(host)) {
+    return;
+  }
+
+  // Resolve host and ensure it does not point to private/internal IPs.
+  try {
+    const lookupResult = await dns.lookup(host, { all: true });
+    for (const addr of lookupResult) {
+      if (isPrivateIp(addr.address)) {
+        throw new Error('Registry host resolves to a private or loopback IP, which is not allowed');
+      }
+    }
+  } catch (err) {
+    if (err.code === 'ENOTFOUND') {
+      throw new Error('Registry host not found');
+    }
+    throw err;
+  }
+}
+
+/**
  * Low-level HTTP GET that returns status, headers, and body without throwing on 4xx
  * @param {string} url - The URL to fetch
  * @param {object} headers - Optional request headers
@@ -243,6 +320,9 @@
  */
 async function getImageConfig(registry, repo, tag) {
   const registryHost = registry === 'docker.io' ? 'registry-1.docker.io' : registry;
+
+  // SSRF protection: ensure the resolved registry host is allowed.
+  await isRegistryHostAllowed(registryHost);
   
   // First, fetch the manifest to get the config digest
   const acceptHeaders = {
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
@cmyers-mieweb
Copy link
Collaborator

Talked with Robert a bit and suggested adding some core packages to the Dockerfile, whether it be anything from tmux, jq, git, etc. Items that are commonly used to save time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants