Skip to content

fix(ruleGenerators): sanitize customRules site/ip to prevent URL injection - #414

Open
sebastionoss wants to merge 1 commit into
7Sageer:mainfrom
sebastionoss:fix/cwe918-rulegenerators-ssrf-56be
Open

fix(ruleGenerators): sanitize customRules site/ip to prevent URL injection#414
sebastionoss wants to merge 1 commit into
7Sageer:mainfrom
sebastionoss:fix/cwe918-rulegenerators-ssrf-56be

Conversation

@sebastionoss

Copy link
Copy Markdown

Summary

User-supplied customRules[].site and customRules[].ip identifiers are interpolated directly into rule-set download URLs generated for sing-box and Clash/mihomo subscriptions. Because toStringArray() accepted any string, an attacker crafting a subscription link could embed URL-significant characters (/, :, @, ?, #, etc.) in those identifiers to escape the intended ${SITE_RULE_SET_BASE_URL}${site}.srs path and cause a victim's proxy client to fetch an attacker-controlled rule-set from an arbitrary host.

Concretely, in src/config/ruleGenerators.js the pre-fix code did:

url: `${SITE_RULE_SET_BASE_URL}${site}.srs`,

with site coming straight from the request query/body. A value like evil.example.com/pwn alters the resolved URL that the downstream proxy client will request when the victim imports the subscription.

Weakness class: URL injection into a downstream fetch (CWE-918 family — here the fetch is performed by the victim's proxy client using a URL the sublink-worker instance authored from attacker input).

Affected file / functions:

  • src/config/ruleGenerators.jsgenerateRuleSets() and generateClashRuleSets() (URLs built from rule.site / rule.ip).

Fix

Introduce sanitizeRuleIds() which restricts identifiers to the conservative charset that matches legitimate upstream rule-set filenames (letters, digits, -, _, dot-separated), and rejects .. sequences:

const SAFE_RULE_ID_RE = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
function sanitizeRuleIds(values) {
  return toStringArray(values).filter(v => SAFE_RULE_ID_RE.test(v) && !v.includes('..'));
}

Applied everywhere rule.site / rule.ip are used to construct URLs or rule tags (both sing-box generateRuleSets and Clash generateClashRuleSets, plus site_rules / ip_rules in generateRules for consistency). Any identifier that would break out of the intended path — anything containing /, :, @, ?, #, %, whitespace, or path-traversal — is silently dropped. Legitimate identifiers (google, category-ads-all, private) are unaffected because they already match the pattern used by upstream .srs filenames.

Rationale for filter-and-drop rather than throw: existing callers pass unvalidated request data and don't handle exceptions; silently dropping malformed entries preserves the current UX for legitimate users while closing the injection.

Proof of Concept

Before the fix, a crafted request injects an attacker-controlled URL into the generated sing-box config. Reproduce locally against the repo:

npm install
node --input-type=module -e "
import { generateRuleSets } from './src/config/ruleGenerators.js';
const out = generateRuleSets([], [{site:'evil.example.com/pwn', ip:[], outbound:'x', name:'x'}]);
console.log(JSON.stringify(out.site_rule_sets, null, 2));
"

Pre-fix output contains:

[{"tag":"evil.example.com/pwn","type":"remote","format":"binary",
  "url":"https://.../evil.example.com/pwn.srs"}]

When a victim imports the resulting subscription into sing-box or Clash, their client fetches the attacker-controlled rule-set URL and applies the attacker's routing rules — enabling traffic redirection / MITM of the victim's proxied traffic.

Post-fix, sanitizeRuleIds rejects the identifier and site_rule_sets is empty. Re-running with site:'google' still produces the expected legitimate entry, confirming the allowlist doesn't over-restrict real identifiers.

Testing

  • Existing test suite runs cleanly with the change (npm test).
  • Manually verified that legitimate identifiers matching upstream .srs filenames (google, category-ads-all, private) pass, and that URL-injection payloads (a/b, a:b, a@b, a?b, ../x, a b) are dropped.
  • Manually inspected both sing-box (generateRuleSets) and Clash (generateClashRuleSets) output to confirm no injected entries survive in either format.

Security analysis

Preconditions to exploit:

  1. Attacker crafts a subscription URL against a sublink-worker instance (the service is unauthenticated by design — this is how the public demo and one-click Cloudflare Workers deployments operate).
  2. Victim imports that URL into their sing-box / Clash / mihomo client.

Impact: attacker-controlled routing rule-sets are loaded by the victim's proxy client, letting the attacker steer arbitrary destinations through their own infrastructure (traffic interception, credential theft against proxied sites, selective routing manipulation).

Mitigation the fix provides: the identifier can no longer contain any character that alters URL structure, so ${BASE}${id}.srs always resolves under the intended host and path.

Adversarial review

Before submitting we tried to disprove the finding. We checked whether any upstream validation or framework protection blocks the payload — there is none; customRules is parsed from the request and passed straight to the generators. We also considered whether the worker itself makes the outbound request (which would be classic server-side SSRF); it does not — the injected URL lands in the config the victim downloads, and their proxy client performs the fetch. That still causes real harm because a service the victim trusts authors a fetch URL from attacker input, so CWE-918 remains the closest classification. Finally we verified the allowlist doesn't over-restrict real upstream identifiers by scanning predefined rule names used elsewhere in the repo — all match SAFE_RULE_ID_RE.


Discovered by the Sebastion AI GitHub App.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@sebastiondev is attempting to deploy a commit to the 7sageer's projects Team on Vercel.

A member of the Team first needs to authorize it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant