php2shell-scanner.git

php2shell-scanner

~ Check PHP2Shell (CVE-2026-109618, CVE-2026-109619) from inside your server: distribution backports, missed restarts and interim mitigations, verdicts computed in the browser.
git clone https://github.com/debuman/php2shell-scanner.git

site/lib/version.js (7566B)

      1 // Compares upstream PHP versions and Debian/Ubuntu package versions.
      2 
      3 export function parsePhpVersion(text) {
      4   const m = /^(\d+)\.(\d+)\.(\d+)(.*)$/.exec(String(text || '').trim());
      5   if (!m) return null;
      6   const suffix = m[4];
      7   // Pre-releases: "8.6.0RC3", "8.5.12-dev" (a branch snapshot before that release), "8.2.0~rc1-1" (a Debian package of one),
      8   // "8.5.12_rc1-r0" and "8.5.0_pre1" (Alpine writes them with an underscore; its "_p1" is a patch level, not a pre-release).
      9   // In a Debian version any other "~" suffix of the upstream part ("8.4.27~git20261001-1") sorts before the release as
     10   // well: it is a snapshot taken before it. Repack markers ("~dfsg") are not pre-releases.
     11   const pre = /^[-.~_]?((?:alpha|beta|RC|dev|pre)\d*)/i.exec(suffix)
     12     || /^~(?!(?:dfsg|ds|repack)\d*(?:$|[-+~]))([^-+~]+)/i.exec(suffix);
     13   const rest = pre ? suffix.slice(pre[0].length) : suffix;
     14   return {
     15     major: Number(m[1]),
     16     minor: Number(m[2]),
     17     patch: Number(m[3]),
     18     branch: `${m[1]}.${m[2]}`,
     19     upstream: `${m[1]}.${m[2]}.${m[3]}`,
     20     pre: pre ? pre[1] : '',
     21     // The distribution revision, if one is attached, as in "-1ubuntu2.20" or "-1~deb12u1"
     22     revision: rest.startsWith('-') ? rest.slice(1) : '',
     23     raw: String(text).trim(),
     24   };
     25 }
     26 
     27 // Compares major.minor.patch only. For the same numbers, a pre-release such as RC sorts lower.
     28 export function comparePhp(a, b) {
     29   const pa = typeof a === 'string' ? parsePhpVersion(a) : a;
     30   const pb = typeof b === 'string' ? parsePhpVersion(b) : b;
     31   if (!pa || !pb) throw new Error(`Cannot parse PHP version: ${a} / ${b}`);
     32   for (const k of ['major', 'minor', 'patch']) {
     33     if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
     34   }
     35   if (pa.pre && !pb.pre) return -1;
     36   if (!pa.pre && pb.pre) return 1;
     37   return 0;
     38 }
     39 
     40 // A direct port of dpkg's version comparison (lib/dpkg/version.c).
     41 export function compareDebian(a, b) {
     42   const pa = splitDebian(a);
     43   const pb = splitDebian(b);
     44   if (pa.epoch !== pb.epoch) return pa.epoch < pb.epoch ? -1 : 1;
     45   const c = verrevcmp(pa.upstream, pb.upstream);
     46   if (c !== 0) return c;
     47   return verrevcmp(pa.revision, pb.revision);
     48 }
     49 
     50 export function splitDebian(version) {
     51   let v = String(version || '').trim();
     52   let epoch = 0;
     53   const colon = v.indexOf(':');
     54   if (colon >= 0) {
     55     epoch = Number.parseInt(v.slice(0, colon), 10) || 0;
     56     v = v.slice(colon + 1);
     57   }
     58   const hyphen = v.lastIndexOf('-');
     59   if (hyphen < 0) return { epoch, upstream: v, revision: '' };
     60   return { epoch, upstream: v.slice(0, hyphen), revision: v.slice(hyphen + 1) };
     61 }
     62 
     63 const isDigit = (ch) => ch >= '0' && ch <= '9';
     64 
     65 function order(ch) {
     66   if (ch === '') return 0;
     67   if (isDigit(ch)) return 0;
     68   if (/[A-Za-z]/.test(ch)) return ch.charCodeAt(0);
     69   if (ch === '~') return -1;
     70   return ch.charCodeAt(0) + 256;
     71 }
     72 
     73 function verrevcmp(a, b) {
     74   let i = 0;
     75   let j = 0;
     76   const at = (s, k) => (k < s.length ? s[k] : '');
     77   while (i < a.length || j < b.length) {
     78     let firstDiff = 0;
     79     while ((i < a.length && !isDigit(a[i])) || (j < b.length && !isDigit(b[j]))) {
     80       const ac = order(at(a, i));
     81       const bc = order(at(b, j));
     82       if (ac !== bc) return ac < bc ? -1 : 1;
     83       i++;
     84       j++;
     85     }
     86     while (at(a, i) === '0') i++;
     87     while (at(b, j) === '0') j++;
     88     while (isDigit(at(a, i)) && isDigit(at(b, j))) {
     89       if (!firstDiff) firstDiff = a.charCodeAt(i) - b.charCodeAt(j);
     90       i++;
     91       j++;
     92     }
     93     if (isDigit(at(a, i))) return 1;
     94     if (isDigit(at(b, j))) return -1;
     95     if (firstDiff) return firstDiff < 0 ? -1 : 1;
     96   }
     97   return 0;
     98 }
     99 
    100 // Repack markers in a Debian upstream version ("5.5.9+dfsg"). PHP_VERSION never carries them.
    101 const REPACK = /[+~](?:dfsg|ds|repack)\d*$/i;
    102 
    103 // The upstream part of a package version or a PHP_VERSION string, without epoch and repack
    104 // markers: "5.5.9+dfsg-1ubuntu4.29+esm18" -> "5.5.9", "1:8.2.30-1.el9_6" -> "8.2.30".
    105 export function debUpstream(version) {
    106   return splitDebian(version).upstream.replace(REPACK, '');
    107 }
    108 
    109 // A package version or PHP_VERSION string in a form that compares like for like: the epoch
    110 // (never in PHP_VERSION; PHP source packages do not use one) and repack markers are dropped.
    111 export function debComparable(version) {
    112   const { upstream, revision } = splitDebian(version);
    113   const up = upstream.replace(REPACK, '');
    114   return revision ? `${up}-${revision}` : up;
    115 }
    116 
    117 // Builds from Ondřej Surý's repository (deb.sury.org) follow upstream versions directly.
    118 export function isSuryBuild(version) {
    119   return /deb\.sury\.org|\+0~20\d{6}|\.gbp[0-9a-f]+/.test(String(version || ''));
    120 }
    121 
    122 // The build date of a deb.sury.org Debian build ("8.1.34-9+0~20261021.82+debian12~1.gbp4e430a" -> "2026-10-21"), or
    123 // null. Its Ubuntu builds ("+ubuntu22.04.1+deb.sury.org+1") carry no date.
    124 export function suryBuildDate(version) {
    125   const m = /\+0~(20\d{2})(\d{2})(\d{2})/.exec(String(version || ''));
    126   return m ? `${m[1]}-${m[2]}-${m[3]}` : null;
    127 }
    128 
    129 // rpm's version comparison (rpmvercmp in rpmio/rpmvercmp.c) of one version or release string.
    130 function rpmvercmp(a, b) {
    131   if (a === b) return 0;
    132   const isAlnum = (ch) => /[A-Za-z0-9]/.test(ch);
    133   const isAlpha = (ch) => /[A-Za-z]/.test(ch);
    134   let i = 0;
    135   let j = 0;
    136   while (i < a.length || j < b.length) {
    137     while (i < a.length && !isAlnum(a[i]) && a[i] !== '~' && a[i] !== '^') i++;
    138     while (j < b.length && !isAlnum(b[j]) && b[j] !== '~' && b[j] !== '^') j++;
    139     // "~" sorts before everything, even the end of the string.
    140     if (a[i] === '~' || b[j] === '~') {
    141       if (a[i] !== '~') return 1;
    142       if (b[j] !== '~') return -1;
    143       i++;
    144       j++;
    145       continue;
    146     }
    147     // "^" sorts after the end of the string but before anything else.
    148     if (a[i] === '^' || b[j] === '^') {
    149       if (i >= a.length) return -1;
    150       if (j >= b.length) return 1;
    151       if (a[i] !== '^') return 1;
    152       if (b[j] !== '^') return -1;
    153       i++;
    154       j++;
    155       continue;
    156     }
    157     if (i >= a.length || j >= b.length) break;
    158     const si = i;
    159     const sj = j;
    160     const numeric = isDigit(a[i]);
    161     if (numeric) {
    162       while (i < a.length && isDigit(a[i])) i++;
    163       while (j < b.length && isDigit(b[j])) j++;
    164     } else {
    165       while (i < a.length && isAlpha(a[i])) i++;
    166       while (j < b.length && isAlpha(b[j])) j++;
    167     }
    168     let x = a.slice(si, i);
    169     let y = b.slice(sj, j);
    170     // A numeric segment is newer than an alphabetic one.
    171     if (!y.length) return numeric ? 1 : -1;
    172     if (numeric) {
    173       x = x.replace(/^0+/, '');
    174       y = y.replace(/^0+/, '');
    175       if (x.length !== y.length) return x.length < y.length ? -1 : 1;
    176     }
    177     if (x !== y) return x < y ? -1 : 1;
    178   }
    179   if (i >= a.length && j >= b.length) return 0;
    180   return i >= a.length ? -1 : 1;
    181 }
    182 
    183 // Compares two rpm versions "[epoch:]version-release" as rpm does. A missing epoch is 0; when one side has no release,
    184 // only the versions are compared.
    185 export function compareRpm(a, b) {
    186   const split = (s) => {
    187     let v = String(s || '').trim();
    188     let epoch = 0;
    189     const colon = v.indexOf(':');
    190     if (colon >= 0) {
    191       epoch = Number.parseInt(v.slice(0, colon), 10) || 0;
    192       v = v.slice(colon + 1);
    193     }
    194     const hyphen = v.lastIndexOf('-');
    195     return hyphen < 0 ? { epoch, version: v, release: '' } : { epoch, version: v.slice(0, hyphen), release: v.slice(hyphen + 1) };
    196   };
    197   const pa = split(a);
    198   const pb = split(b);
    199   if (pa.epoch !== pb.epoch) return pa.epoch < pb.epoch ? -1 : 1;
    200   const c = rpmvercmp(pa.version, pb.version);
    201   if (c !== 0 || !pa.release || !pb.release) return c;
    202   return rpmvercmp(pa.release, pb.release);
    203 }