DEV Community

ProxySmart
ProxySmart

Posted on

Parsing Proxy Lists Safely in Node.js: Formats, Validation, and Secret-Safe Logs

Proxy lists arrive in many shapes:

203.0.113.10:8080
user:password@203.0.113.10:8080
http://user:password@203.0.113.10:8080
socks5://[2001:db8::10]:1080
Enter fullscreen mode Exit fullscreen mode

They look simple until a parser splits on every colon, leaks credentials into logs, accepts an impossible port, or confuses syntax validation with a real connectivity check.

This guide builds a small, defensive parser for Node.js and explains which guarantees it can—and cannot—provide.

If you only need a quick browser-side syntax check, the Proxy Format Checker validates one or many entries without connecting to them. The rest of this article shows how to implement the same separation of concerns in code.

Start with a clear contract

A format validator should answer:

  • Is the line structurally valid?
  • Which protocol is requested?
  • Are host and port present?
  • Can credentials be decoded safely?
  • Can the value be normalized into a canonical representation?

It should not claim that:

  • the endpoint is online;
  • authentication succeeds;
  • the exit IP matches the host;
  • latency is acceptable;
  • the IP is clean or suitable for a specific service.

Those require a controlled network request and a separate health-check pipeline.

Supported formats

For a small production parser, define an explicit allowlist:

const ALLOWED_PROTOCOLS = new Set([
  "http:",
  "https:",
  "socks4:",
  "socks5:",
]);
Enter fullscreen mode Exit fullscreen mode

Do not silently accept an unknown scheme. A typo such as sock5:// should fail early instead of being treated as a hostname.

A defensive parser

const ALLOWED_PROTOCOLS = new Set([
  "http:",
  "https:",
  "socks4:",
  "socks5:",
]);

export function parseProxyLine(input, defaultProtocol = "http:") {
  const raw = String(input ?? "").trim();

  if (!raw) {
    return { ok: false, code: "empty", message: "Proxy line is empty" };
  }

  // Reject control characters before URL parsing.
  if (/[\u0000-\u001f\u007f]/.test(raw)) {
    return {
      ok: false,
      code: "control_character",
      message: "Control characters are not allowed",
    };
  }

  const hasScheme = /^[a-z][a-z0-9+.-]*:///i.test(raw);
  const candidate = hasScheme ? raw : `${defaultProtocol}//${raw}`;

  let url;
  try {
    url = new URL(candidate);
  } catch {
    return {
      ok: false,
      code: "invalid_url",
      message: "The proxy does not match a supported URL format",
    };
  }

  if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
    return {
      ok: false,
      code: "unsupported_protocol",
      message: `Unsupported proxy protocol: ${url.protocol}`,
    };
  }

  if (!url.hostname) {
    return { ok: false, code: "missing_host", message: "Host is required" };
  }

  if (!url.port) {
    return { ok: false, code: "missing_port", message: "Port is required" };
  }

  const port = Number(url.port);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    return {
      ok: false,
      code: "invalid_port",
      message: "Port must be between 1 and 65535",
    };
  }

  let username = "";
  let password = "";

  try {
    username = decodeURIComponent(url.username);
    password = decodeURIComponent(url.password);
  } catch {
    return {
      ok: false,
      code: "invalid_credentials_encoding",
      message: "Credentials contain invalid percent encoding",
    };
  }

  return {
    ok: true,
    value: {
      protocol: url.protocol.slice(0, -1),
      host: url.hostname,
      port,
      username,
      password,
      hasCredentials: Boolean(username || password),
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Why use URL?

The WHATWG URL parser correctly handles:

  • percent-encoded usernames and passwords;
  • bracketed IPv6 addresses;
  • explicit schemes;
  • special characters that break naive split(":") logic.

It is still your responsibility to enforce a protocol allowlist and require a port.

Avoid the classic colon-splitting bug

This code is fragile:

const [host, port] = line.split(":");
Enter fullscreen mode Exit fullscreen mode

It breaks for:

  • IPv6 addresses, which contain many colons;
  • credentials with encoded or unexpected characters;
  • full URLs with a scheme;
  • whitespace and malformed input.

For IPv6, require brackets:

socks5://[2001:db8::10]:1080
Enter fullscreen mode Exit fullscreen mode

An unbracketed IPv6 shorthand is ambiguous and should be rejected with a helpful error.

Parse a batch without losing line numbers

export function parseProxyList(text) {
  return String(text ?? "")
    .split(/
?
/)
    .map((line, index) => ({
      lineNumber: index + 1,
      raw: line,
      result: parseProxyLine(line),
    }))
    .filter((entry) => entry.raw.trim().length > 0);
}
Enter fullscreen mode Exit fullscreen mode

Keeping the original line number makes errors actionable:

const entries = parseProxyList(proxyText);

for (const entry of entries) {
  if (!entry.result.ok) {
    console.warn(
      `Line ${entry.lineNumber}: ${entry.result.code}`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the warning does not print the raw input.

Secret-safe logging

A proxy string may contain a username, password, customer identifier, or session token. Treat the entire raw value as a secret.

Prefer structured metadata:

function proxyLogFields(parsed) {
  if (!parsed.ok) {
    return { valid: false, errorCode: parsed.code };
  }

  const { protocol, host, port, hasCredentials } = parsed.value;

  return {
    valid: true,
    protocol,
    host,
    port,
    hasCredentials,
  };
}
Enter fullscreen mode Exit fullscreen mode

If the host itself is sensitive in your system, hash or redact it too. Do not assume that removing only the password makes a log safe.

Canonical display without credentials

export function formatPublicProxy(parsed) {
  if (!parsed.ok) return null;

  const { protocol, host, port } = parsed.value;
  const displayHost = host.includes(":") ? `[${host}]` : host;

  return `${protocol}://${displayHost}:${port}`;
}
Enter fullscreen mode Exit fullscreen mode

This is suitable for UI previews and diagnostic messages because credentials are never included.

Keep validation and connectivity separate

A clean pipeline has three stages:

  1. Parse — convert untrusted text into a typed object.
  2. Validate — enforce protocol, host, port, and policy rules.
  3. Probe — optionally attempt a connection with strict timeouts.

Do not run probes inside form validation. Network checks are slower, can be abused for server-side request forgery, and require destination controls.

If you later add connectivity testing, consider:

  • an allowlist or denylist for private and reserved destinations;
  • connection and total timeouts;
  • bounded concurrency;
  • no automatic retries for authentication failures;
  • credential-safe logs;
  • explicit user authorization.

Test cases worth keeping

const cases = [
  ["203.0.113.10:8080", true],
  ["http://203.0.113.10:8080", true],
  ["socks5://user:pass@203.0.113.10:1080", true],
  ["socks5://[2001:db8::10]:1080", true],
  ["203.0.113.10", false],
  ["ftp://203.0.113.10:21", false],
  ["203.0.113.10:99999", false],
  ["", false],
];

for (const [input, expected] of cases) {
  const actual = parseProxyLine(input).ok;
  console.assert(actual === expected, input);
}
Enter fullscreen mode Exit fullscreen mode

Add project-specific cases for internationalized hostnames, encoded credentials, maximum input length, and duplicate entries.

Practical checklist

Before accepting a proxy list:

  • trim lines and ignore blank ones;
  • limit total input size and number of entries;
  • allow only known protocols;
  • require a valid port;
  • require brackets around IPv6 hosts;
  • never print raw proxy strings in logs;
  • distinguish syntax validity from connectivity;
  • protect any probe service against SSRF and unbounded concurrency.

Final thought

Proxy parsing is not hard because the happy path is complicated. It is hard because a one-line shortcut quietly mixes syntax, secrets, networking, and trust.

Keep those responsibilities separate, return machine-readable error codes, and make the safe behavior the default.

Top comments (0)