Files
vulncheck/tests/test_teams_classic.py
T
vulncheck 81bafda04a fix(teams): skip the 1.0.0.0 placeholder, keep Teams Panel/Phone off Teams
Field case: an Intune host lists "MSTeams 1.0.0.0" (CN=Microsoft
Corporation). It drew CVE-2023-4863, -29328, -29330, CVE-2022-21965 and
CVE-2025-53783, plus the Teams classic EOL.

1.0.0.0 names no build. It is the new client's MSIX package (Intune's
"MSTeams", Wazuh's WindowsApps\MSTeams_1.0.0.0_...) with no version read;
no desktop Teams shipped as 1.0.0.0 and classic was never MSIX. Four parts
under 100, so it passed the scheme check and sat below every classic and
Android bound. teams_placeholder_version (1.0.0.0, 1.0, 1, 0.0.0.0) is now
skipped by the cvelistV5 and NVD scans and by _teams_classic. 0a73eafb had
read it as classic; "MSTeams" now counts as the new client for EOL, like
"Microsoft Teams". Real classic builds (1.5.00.31168) keep both.

CVE-2025-53783 came through teams_panels: the NVD filter matched the
vendor:product token as a substring, so microsoft:teams also took
teams_panels and teams_phones (1.0.9x device builds), and Teams for
Android (1.0.0.2025...) got their CVE and the Panel fix 1.0.97.2025102203.
Teams now takes its CPE exactly. Other products keep the prefix match;
Adobe relies on it (acrobat_dc). NVD cache prefix v5.
2026-10-08 13:15:00 +02:00

248 lines
11 KiB
Python

"""Classic Teams (1.x) — run: python tests/test_teams_classic.py
Field case: an Intune host lists "Microsoft Teams" 1.5.00.31168 and "Teams
Machine-Wide Installer" 1.5.0.31168. Neither CVE-2023-29328 nor -29330
(Teams for Desktop, fixed 1.6.00.18681) reached it from cvelistV5.
* cvelistV5: Teams is a strict-scheme key, so bounds in another build shape
(iOS 7.10.1 against new Teams' 26183.x) are not compared. The check ran
over the floor too, and Microsoft writes the desktop floor as "1.0.0":
three parts against 1.5.00.31168's four, so every classic Teams range was
refused. Only the upper bounds decide the scheme now.
* NVD: the same CVEs did match, but showed the macOS fix (1.6.00.17554,
the first entry) instead of the desktop's. A platform-neutral entry's fix
now wins. And CVE-2023-24881: NVD lost Microsoft's floor and says
"< 2.10.1" for any platform; the record says 2.0.0 .. < 2.10.1, MSRC's
only fix is 2.10.1. 1.x is not that product, so the NVD Teams entries
get the same scheme check (bound 2.10.1 has three parts).
The Machine-Wide Installer stays out: it stages the client into each profile
and keeps its own version while the client updates itself. The AppX
name "MSTeams" Intune reports and a bare "Teams" are the client and now match.
Fixtures are the real record fields.
"""
import json
import os
import sys
import tempfile
import zipfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services import app_cve_scanner_service as cpe # noqa: E402
from app.services import cvelistv5_scan_service as c5 # noqa: E402
def _rng(product, lo, lt):
return {"vendor": "Microsoft", "product": product, "versions": [
{"version": lo, "lessThan": lt, "versionType": "custom", "status": "affected"}]}
_29328 = [_rng("Microsoft Teams for Android", "1.0.0", "1.0.0.2023070204"),
_rng("Microsoft Teams for Desktop", "1.0.0", "1.6.00.18681"),
_rng("Microsoft Teams for iOS", "2.0.0", "5.12.1"),
_rng("Microsoft Teams for Mac", "1.0.0.0", "1.6.00.17554")]
_24881 = [dict(_rng("Microsoft Teams", "2.0.0", "2.10.1"), platforms=["Unknown"])]
# CVE-2025-49731: new Teams, MSRC's 11-digit build and the mobile bounds.
_49731 = [_rng("Microsoft Teams for Android", "1.0.0", "1.0.0.2025112902"),
_rng("Microsoft Teams for iOS", "7.0.0", "7.10.1"),
_rng("Microsoft Teams", "0", "25060212643")]
CVE5 = {"CVE-2023-29328": _29328, "CVE-2023-29330": _29328,
"CVE-2023-24881": _24881, "CVE-2025-49731": _49731}
def _m(tsw, end, edition="*"):
return {"vulnerable": True, "versionEndExcluding": end,
"criteria": f"cpe:2.3:a:microsoft:teams:*:*:*:*:{edition}:{tsw}:*:*"}
_N29328 = [_m("android", "1.0.0.2023070204"), _m("macos", "1.6.00.17554"),
_m("-", "1.6.00.18681"), _m("iphone_os", "5.12.1")]
NVD = {"vulnerabilities": [
{"cve": {"id": cve, "configurations": [{"nodes": [{"cpeMatch": ms}]}]}}
for cve, ms in (
("CVE-2023-29328", _N29328), ("CVE-2023-29330", _N29328),
("CVE-2023-24881", [_m("*", "2.10.1")]),
# libwebp, on KEV: desktop edition, classic build bound.
("CVE-2023-4863", [_m("macos", "1.6.00.26463"),
_m("*", "1.6.00.26474", edition="desktop")]),
("CVE-2025-53783", [_m("android", "1.0.0.2025102802"), _m("iphone_os", "7.10.1"),
_m("macos", "25122.1207.3700.1444"),
_m("*", "25122.1415.3698.6812", edition="desktop"),
{"vulnerable": True, "versionEndExcluding": "1.0.97.2025102203",
"criteria": "cpe:2.3:a:microsoft:teams_panels:*:*:*:*:*:*:*:*"},
{"vulnerable": True, "versionEndExcluding": "1.0.94.2025168802",
"criteria": "cpe:2.3:a:microsoft:teams_phones:*:*:*:*:*:*:*:*"}]),
# No platform on the bounded entry: Android / Rooms numbering.
("CVE-2022-21965", [_m("*", "1.0.94.20000")]),
)]}
class FakeAsset:
id = 1
operating_system = "Windows 11 Enterprise"
def _build_index():
"""Run the REAL index build over a ZIP shaped like cvelistV5's."""
fd, path = tempfile.mkstemp(suffix=".zip")
os.close(fd)
with zipfile.ZipFile(path, "w") as zf:
for cve_id, aff in CVE5.items():
year, num = cve_id.split("-")[1:]
zf.writestr(f"cvelistV5-main/cves/{year}/{num[:-3]}xxx/{cve_id}.json",
json.dumps({"cveMetadata": {"cveId": cve_id},
"containers": {"cna": {"affected": aff}}}))
orig = (c5._ZIP_PATH, c5._ensure_zip, c5._store_index, c5._merge_mozilla_mfsa,
c5._merge_chrome_releases)
c5._ZIP_PATH = path
c5._ensure_zip = lambda force=False: True
c5._store_index = lambda db, index: None
c5._merge_mozilla_mfsa = lambda *a, **kw: None
c5._merge_chrome_releases = lambda *a, **kw: None
try:
return c5.build_product_index(None)
finally:
(c5._ZIP_PATH, c5._ensure_zip, c5._store_index, c5._merge_mozilla_mfsa,
c5._merge_chrome_releases) = orig
os.unlink(path)
def _scan5(index, name, version, os_name="Windows 11 Enterprise"):
class Host(FakeAsset):
operating_system = os_name
found = []
orig = cpe._upsert
cpe._upsert = lambda db, asset, n, v, c, new_ids, **kw: (
found.append((c["cve"], c["fixed"])), new_ids.append(len(new_ids)))
try:
c5.scan_asset(None, Host(), [{"name": name, "version": version}],
index, touched=set())
finally:
cpe._upsert = orig
return sorted(found)
def _nvd(version):
"""The REAL NVD decision, as a Windows host would keep it."""
return sorted((c["cve"], c["fixed"]) for c in _query(version)
if cpe._platform_ok(c.get("tsw"), "windows"))
def _query(version):
"""The REAL NVD query over the fixture response."""
class _R:
status_code = 200
def json(self):
return NVD
class _C:
def __init__(self, *a, **k):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get(self, *a, **k):
return _R()
orig = cpe.httpx.Client, cpe.time.sleep
cpe.httpx.Client, cpe.time.sleep = _C, (lambda s: None)
try:
return cpe._query_nvd_cpe("cpe:2.3:a:microsoft:teams", version)
finally:
cpe.httpx.Client, cpe.time.sleep = orig
def _scan_nvd(os_name, version):
"""scan_asset_packages over the fixture, as a host on `os_name`."""
class Host(FakeAsset):
operating_system = os_name
found = []
orig = cpe._upsert, cpe.lookup_cves
cpe._upsert = lambda db, asset, n, v, c, new_ids, **kw: (
found.append((c["cve"], c["fixed"])), new_ids.append(len(new_ids)))
cpe.lookup_cves = lambda db, entry, ver, **kw: _query(ver)
try:
cpe.scan_asset_packages(None, Host(), [{"name": "Microsoft Teams", "version": version}],
touched=set())
finally:
cpe._upsert, cpe.lookup_cves = orig
return sorted(found)
def demo():
# --- names ------------------------------------------------------------------
for name in ("Microsoft Teams", "Microsoft Teams classic", "MSTeams", "Teams"):
assert c5.resolve(name) == "teams", name
assert (cpe.resolve_product(name) or {}).get("key") == "cpe:microsoft:teams", name
for other in ("Teams Machine-Wide Installer", "Microsoft Teams Meeting Add-in for "
"Microsoft Office", "Veeam Explorer for Microsoft Teams", "Teams Rooms"):
assert c5.resolve(other) is None, other
assert cpe.resolve_product(other) is None, other
# --- cvelistV5 ----------------------------------------------------------------
index = _build_index()
want = [("CVE-2023-29328", "1.6.00.18681"), ("CVE-2023-29330", "1.6.00.18681")]
for name in ("Microsoft Teams", "MSTeams", "Teams"):
assert _scan5(index, name, "1.5.00.31168") == want, name
assert _scan5(index, "Microsoft Teams", "1.6.00.18681") == []
# A Mac on its own fix: the record's Mac range is not indexed, the
# desktop's is not the Mac's. NVD has the macOS entries.
assert _scan5(index, "Microsoft Teams", "1.6.00.17554", "macOS 13.6") == []
# New Teams: the 11-digit MSRC build and the mobile bounds stay uncompared.
assert _scan5(index, "MSTeams", "26183.1903.4892.4448") == []
# --- NVD --------------------------------------------------------------------
assert _nvd("1.5.00.31168") == [("CVE-2023-29328", "1.6.00.18681"),
("CVE-2023-29330", "1.6.00.18681"),
("CVE-2023-4863", "1.6.00.26474")]
assert _nvd("1.6.00.18681") == [("CVE-2023-4863", "1.6.00.26474")]
assert _nvd("1.6.00.26474") == []
# New Teams keeps its desktop CVE.
assert _nvd("25122.1405.3600.1111") == [("CVE-2025-53783", "25122.1415.3698.6812")]
assert _nvd("26183.1903.4892.4448") == []
# The fix shown is the host's own platform's: Windows the desktop build,
# a Mac its own (CVE-2023-4863: 1.6.00.26463 on macOS).
assert _scan_nvd("Windows 11 Enterprise", "1.5.00.31168") == _nvd("1.5.00.31168")
mac = dict(_scan_nvd("macOS 13.6", "1.5.00.31168"))
assert mac["CVE-2023-4863"] == "1.6.00.26463", mac
assert mac["CVE-2023-29328"] == "1.6.00.17554", mac
# --- placeholder ------------------------------------------------------------
# Intune: "MSTeams 1.0.0.0" — the new client's MSIX package without a
# build. Four parts and below every classic / Android bound, so it drew
# CVE-2023-29328/-29330 (cvelistV5 + NVD), -4863, CVE-2022-21965 and,
# via teams_panels' 1.0.97.x, CVE-2025-53783.
for version in ("1.0.0.0", "1.0.0", "1", "0.0.0.0"):
for name in ("MSTeams", "Microsoft Teams", "Teams"):
assert _scan5(index, name, version) == [], (name, version)
assert _scan_nvd("Windows 11 Enterprise", version) == [], version
# …while a real classic build still gets them, and the old bound's
# Android numbering (1.0.94.x) is not a placeholder.
assert _scan_nvd("Windows 11 Enterprise", "1.5.00.31168") == _nvd("1.5.00.31168")
assert not cpe.teams_placeholder_version("1.0.94.19999")
# --- Teams Panel / Phone --------------------------------------------------
# Their own CPEs (teams_panels, teams_phones), not Teams. Teams for
# Android (1.0.0.2025…) sits below their 1.0.9x bounds and picked them up,
# fix included. It gets the Android entry only.
c = next(c for c in _query("1.0.0.2025102801") if c["cve"] == "CVE-2025-53783")
assert c["fixes"] == {"android": "1.0.0.2025102802"}, c
print("ok classic Teams 1.5.00.31168 gets CVE-2023-29328/-29330 from both paths "
"with the desktop fix, CVE-2023-24881 (2.x only) stays off it")
def test_teams_classic():
demo()
if __name__ == "__main__":
demo()