mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
bench(independent): freeze population v3 (PHP)
freeze.py, extended for v3 and still reading only git history, file lists and fix diffs: - a file the advisory names without a function resolves to the function(s) the fix's diff changes at the vulnerable pin (maintainer's decision 2), recorded with its basis (advisory-named / resolved-from-fix-diff); a new helper inserted at file scope adds no site; - an inferred fix must change a declared site function, else the case drops; - a single-language population gets a strict benign proposal (PHP change, merges judged with all they bring in, pre-vulnerable history as fallback). All 15 cases verified, none dropped (pagible and podlove fixes confirmed). Nine sites in eight cases resolved from diffs; five benign proposals replaced by hand. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
edcf145b00
commit
0a04dbfada
@@ -0,0 +1,561 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"population": "population-v3-php.toml",
|
||||
"cases": [
|
||||
{
|
||||
"id": "gitlist-commandline-cmdi",
|
||||
"repo": "https://github.com/klaussilveira/gitlist",
|
||||
"vulnerable": "f67609d52c1812fa8a7ed80eae5e795cfd72115f",
|
||||
"fixed": "88cf2866083d5f7c20d9d565c45f828a7ad1516b",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sink_file": "src/SCM/System/Git/CommandLine.php",
|
||||
"sink_functions": [
|
||||
"getDefaultBranch"
|
||||
],
|
||||
"sink_sha256_vulnerable": "a0aa9125dabc9748274b3762b9bd1ad117400b1b8d446593afdc3e6f38951e26",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "3f1c45d9f0ee68c517bf89fa3fe65c0ead84351ed5d9329ac5d6a4b53d575344",
|
||||
"fix_changed_files": 2,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "ce5806c849f453d7200c1f488ee6aba19eef9a4c",
|
||||
"tip": "ecc8e18e03d9b547e82b7c7c148cb00ba01c0cc9",
|
||||
"subject": "Merge pull request #902 from zoredache/sort_repositories",
|
||||
"files": [
|
||||
"src/App/Controller/Repository.php"
|
||||
],
|
||||
"lines": 2,
|
||||
"side": "before the vulnerable pin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "qloapps-customermessage-bo-query-sqli",
|
||||
"repo": "https://github.com/Qloapps/QloApps",
|
||||
"vulnerable": "5b4d65cbb8570a4ba46364ee0f2ded79d294ab84",
|
||||
"fixed": "123c97c110b7053ea3297d8e58fe95b3c3536560",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"classes/CustomerMessage.php::searchByName"
|
||||
],
|
||||
"site_basis": {
|
||||
"classes/CustomerMessage.php::searchByName": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"sink_file": "classes/CustomerMessage.php",
|
||||
"sink_functions": [
|
||||
"searchByName"
|
||||
],
|
||||
"sink_sha256_vulnerable": "f4037f17c83d248639dbb6cce36f35e3b7eaae557dd631bc9268a97415c46534",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "0464ba7047c393a2c873f301717258397bfc50c2b4f423e98f33d0c954678d41",
|
||||
"fix_changed_files": 5,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "884563ccff83fe42b4689ba32e605276278c3989",
|
||||
"tip": "e43b45009508eb1465709d4af0f549faacd1d817",
|
||||
"subject": "Merge pull request #1735 from amitsharma322/gli-3007",
|
||||
"files": [
|
||||
"classes/PaymentModule.php",
|
||||
"controllers/admin/AdminOrdersController.php"
|
||||
],
|
||||
"lines": 23,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roundcube-markasjunk-cmd-learn-rce",
|
||||
"repo": "https://github.com/roundcube/roundcubemail",
|
||||
"vulnerable": "a1afb8fd1f00ed4cb9376c072bb5ca5ded64495e",
|
||||
"fixed": "b8f90e28a46d42e79a69568cba897f8f4223d9cd",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"plugins/markasjunk/drivers/cmd_learn.php::_do_salearn"
|
||||
],
|
||||
"site_basis": {
|
||||
"plugins/markasjunk/drivers/cmd_learn.php::_do_salearn": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"sink_file": "plugins/markasjunk/drivers/cmd_learn.php",
|
||||
"sink_functions": [
|
||||
"_do_salearn"
|
||||
],
|
||||
"sink_sha256_vulnerable": "f4f46e54fa8a19d20ab4000e0f363afd544c6f9f73b806c7b8176fd8348dea83",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "3082db4ed8a79b472ebc2b7d42e34db054737297a81cf595b652a6ba4e34f8eb",
|
||||
"fix_changed_files": 2,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "1cddafded973c4bf6faa173accaf8f52ca1ac0d0",
|
||||
"tip": "747f544708ed1d698b677d333346164a9451aeda",
|
||||
"subject": "Simplify",
|
||||
"files": [
|
||||
"tests/Framework/TnefDecoderTest.php"
|
||||
],
|
||||
"lines": 6,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "maccms10-template-blacklist-rce",
|
||||
"repo": "https://github.com/magicblack/maccms10",
|
||||
"vulnerable": "2cabecda29947e8f9ea7cd3aa47ab90ed817605b",
|
||||
"fixed": "71ad3bb29570e110d8e973acff68040a3050ddf0",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"application/admin/controller/Template.php::info"
|
||||
],
|
||||
"site_basis": {
|
||||
"application/admin/controller/Template.php::info": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"sink_file": "application/admin/controller/Template.php",
|
||||
"sink_functions": [
|
||||
"info"
|
||||
],
|
||||
"sink_sha256_vulnerable": "02e0ff0d172a25fc35b5b386c4280cf65bf3980a612cd833020e43f2dddb5d3c",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "310d667aa2a99f554f49649b0ff1549b0c7f35bf1a0c167a86569c49167f5cc9",
|
||||
"fix_changed_files": 1,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "d9f09011a1523d0bfaed5dea9b2139461f2b0ea8",
|
||||
"tip": "668ba418e50e96ab32ac612f3b6528beb172b3b7",
|
||||
"subject": "fix #1356 \u4fee\u6b63\u6ce8\u518cAPI\u6821\u9a8c\u5668\u89e3\u6790\u9519\u8bef\u53ca\u7f3a\u53c2\u62a5\u9519",
|
||||
"files": [
|
||||
"application/api/controller/User.php",
|
||||
"application/common/model/User.php",
|
||||
"tests/regression/user_register_validate.php",
|
||||
"\"\\350\\257\\264\\346\\230\\216\\346\\226\\207\\346\\241\\243/API\\346\\216\\245\\345\\217\\243\\350\\257\\264\\346\\230\\216V2.txt\""
|
||||
],
|
||||
"lines": 166,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mantisbt-tokenizer-eval-rce",
|
||||
"repo": "https://github.com/mantisbt/mantisbt",
|
||||
"vulnerable": "e7041ecc80c8d35f76a7ea1027a8a231244f7dc5",
|
||||
"fixed": "78c0af63d1fe0118004744cab21ca3bf2cea0f5c",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"core/classes/Tokenizer.class.php::__construct"
|
||||
],
|
||||
"site_basis": {
|
||||
"core/classes/Tokenizer.class.php::__construct": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {
|
||||
"adm_config_set.php": "the fix does not change this file"
|
||||
},
|
||||
"sink_file": "core/classes/Tokenizer.class.php",
|
||||
"sink_functions": [
|
||||
"__construct"
|
||||
],
|
||||
"sink_sha256_vulnerable": "cda7f81f7fcb4675c7bf5fc3f8dfd41ccb48ad891e95a85cf5edae90f0cbb1df",
|
||||
"sink_evidence_found": true,
|
||||
"sink_sha256_fixed": "f555c63e2dc15dbef7ff2c4db8d7c47848c8bc7abe981364a11f65c5e44b8fb0",
|
||||
"fix_changed_files": 1,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "088fa767bce246b6d2f083f32d937f22c8de4767",
|
||||
"tip": "d6ca719b591f70acfa711fc41705388557b6ac04",
|
||||
"subject": "Improve Workflow Graph display.",
|
||||
"files": [
|
||||
"workflow_graph_img.php"
|
||||
],
|
||||
"lines": 14,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sharp-editor-iframe-srcdoc-xss",
|
||||
"repo": "https://github.com/code16/sharp",
|
||||
"vulnerable": "67743149309054493ec8f5505173fc57aaff760f",
|
||||
"fixed": "ec509a22c808a5bd9dfad6a0a85c92ce6f411e21",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"src/Utils/Sanitization/FormatsSanitizedValue.php::sanitizer",
|
||||
"src/Utils/Sanitization/FormatsSanitizedValue.php::isEncodingNeeded"
|
||||
],
|
||||
"site_basis": {
|
||||
"src/Utils/Sanitization/FormatsSanitizedValue.php::sanitizer": "resolved-from-fix-diff",
|
||||
"src/Utils/Sanitization/FormatsSanitizedValue.php::isEncodingNeeded": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"sink_file": "src/Utils/Sanitization/FormatsSanitizedValue.php",
|
||||
"sink_functions": [
|
||||
"sanitizer",
|
||||
"isEncodingNeeded"
|
||||
],
|
||||
"sink_sha256_vulnerable": "7e65b0fbcd481c6322f83ae46523b069c4a62383cbf088e7be60be6ace4bcc9a",
|
||||
"sink_evidence_found": true,
|
||||
"sink_sha256_fixed": "59a2845f4a8b419f8504b2cb539087eb3dc3fb8bb0ec7c950330ba951c6c75ad",
|
||||
"fix_changed_files": 2,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "ec509a22c808a5bd9dfad6a0a85c92ce6f411e21",
|
||||
"tip": "7c601fffe930fc321a5d8bcb25cb072d50a6fc08",
|
||||
"subject": "Update version to v9.22.5",
|
||||
"files": [
|
||||
"src/SharpInternalServiceProvider.php"
|
||||
],
|
||||
"lines": 2,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "privatebin-jsonld-request-uri-injection",
|
||||
"repo": "https://github.com/PrivateBin/PrivateBin",
|
||||
"vulnerable": "e0dd4c025c19a182b6a4c6fb77a8bf81ceff6899",
|
||||
"fixed": "75f056dcda955d94c17ec5a4f8c54a9b7bfcee07",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sink_file": "lib/Request.php",
|
||||
"sink_functions": [
|
||||
"getRequestUri"
|
||||
],
|
||||
"sink_sha256_vulnerable": "bd35439be97ac659d94b1d0cde13f70644f99468bfc16ddc2fb214a845bdb7b4",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "ea17a346372b8259bad1ac7edb1268378168d0de0628cd53b948f52038fd9c1e",
|
||||
"fix_changed_files": 4,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "75f056dcda955d94c17ec5a4f8c54a9b7bfcee07",
|
||||
"tip": "3035d6e5d3cceb7f06f7b7bb80968727879c52b0",
|
||||
"subject": "incrementing version",
|
||||
"files": [
|
||||
"CHANGELOG.md",
|
||||
"Makefile",
|
||||
"README.md",
|
||||
"SECURITY.md",
|
||||
"doc/Installation.md",
|
||||
"js/package.json",
|
||||
"lib/Controller.php"
|
||||
],
|
||||
"lines": 18,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openstamanager-group-name-html-injection",
|
||||
"repo": "https://github.com/devcode-it/openstamanager",
|
||||
"vulnerable": "57a2734e2121aa8fcd456eff7c59f03e29e69176",
|
||||
"fixed": "0b88ad1963871abfa5e134004ca0c74c496d8d2f",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sink_file": "modules/utenti/edit.php",
|
||||
"sink_functions": [],
|
||||
"sink_sha256_vulnerable": "8524dd092b2fdbf68339b80e374c4108fd6925a146223b325370f4ee8414a016",
|
||||
"sink_evidence_found": true,
|
||||
"sink_sha256_fixed": "290d5fde2df7713eb592361f3b0abfd3524a3b83bf99ef0fafdf80f0ee29612b",
|
||||
"fix_changed_files": 4,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "0b88ad1963871abfa5e134004ca0c74c496d8d2f",
|
||||
"tip": "1832b85d0e570e0b873c7c23dcfcb6b5d1365a28",
|
||||
"subject": "fix: ricerca riferimenti in importazione fattura per riga con articolo non esistente",
|
||||
"files": [
|
||||
"plugins/importFE/actions.php"
|
||||
],
|
||||
"lines": 2,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wp-geo-my-wp-info-window-lfi",
|
||||
"repo": "https://github.com/Fitoussi/geo-my-wp",
|
||||
"vulnerable": "1dbb7b1672a45a8f88fdcd379ceb07b6cf2beceb",
|
||||
"fixed": "5a768bf1c6e44ded83a65be8789f3587515665ac",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"plugins/posts-locator/includes/gmw-posts-locator-functions.php::gmw_posts_locator_ajax_info_window_loader",
|
||||
"includes/gmw-functions.php::gmw_ajax_info_window_init"
|
||||
],
|
||||
"site_basis": {
|
||||
"plugins/posts-locator/includes/gmw-posts-locator-functions.php::gmw_posts_locator_ajax_info_window_loader": "advisory-named",
|
||||
"includes/gmw-functions.php::gmw_ajax_info_window_init": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"sink_file": "plugins/posts-locator/includes/gmw-posts-locator-functions.php",
|
||||
"sink_functions": [
|
||||
"gmw_posts_locator_ajax_info_window_loader"
|
||||
],
|
||||
"sink_sha256_vulnerable": "605e777dab7653792d5ae2f2ef6c776f76d1fb1a271d0446c65f6ebf1dfb8461",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "32166bf3941e0a5048b58c49884d513a1be9b11722245f3b7d4d072846400ec2",
|
||||
"fix_changed_files": 5,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "9c394482d1509ca9222365a91117a10b19619be8",
|
||||
"tip": "85bcd85541c777b6fd8580cb1915504a2a2b84cf",
|
||||
"subject": "Release 4.5.5.2",
|
||||
"files": [
|
||||
"geo-my-wp.php",
|
||||
"readme.txt"
|
||||
],
|
||||
"lines": 17,
|
||||
"side": "before the vulnerable pin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kirby-realpath-sibling-traversal",
|
||||
"repo": "https://github.com/getkirby/kirby",
|
||||
"vulnerable": "e90257fd7504053e13e93a1e9a5a9a5137aed966",
|
||||
"fixed": "2b6fab950bc89d505ea89576bf71c8de614cc3b2",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sink_file": "src/Filesystem/Dir.php",
|
||||
"sink_functions": [
|
||||
"realpath"
|
||||
],
|
||||
"sink_sha256_vulnerable": "d17a09853356ed1d2e08421e3130a4fd0e68dc6c40f3734c796d425ea135f8d3",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "e0ec9c78eff595b6076ca396646a097a7f37a4f49212c2bc7ef4aa0d68448652",
|
||||
"fix_changed_files": 4,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "bbc7c251efcbb01a72f5786af15964e596dc6c4d",
|
||||
"tip": "65dfca053d78bac574c277404adbc8175b2844d8",
|
||||
"subject": "fix: Improve content salt message",
|
||||
"files": [
|
||||
"i18n/translations/en.json",
|
||||
"tests/Panel/Areas/SystemTest.php"
|
||||
],
|
||||
"lines": 4,
|
||||
"side": "before the vulnerable pin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mineadmin-appstore-identifier-traversal",
|
||||
"repo": "https://github.com/mineadmin/MineAdmin",
|
||||
"vulnerable": "ac0dcad616c5d304c249d1db4a6675f65cf736dc",
|
||||
"fixed": "ca41902a2a5422676227e5088f4cc1dec06044f1",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sink_file": "plugin/mine-admin/app-store/src/Service/Service.php",
|
||||
"sink_functions": [
|
||||
"download",
|
||||
"install",
|
||||
"unInstall"
|
||||
],
|
||||
"sink_sha256_vulnerable": "d06a053c26f7918a179ead518f70d79d60c2211bcbe751185b95cead8a8af22e",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "b4f2cd97704d2eb76eae07287fd2251d5e072de1a5cba333857a7cc1fd23747c",
|
||||
"fix_changed_files": 2,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "1f99cfd595f9621f4be572d970821f3509acfe33",
|
||||
"tip": "15deedde8c106842d9c3584d8680e2f65adaaac1",
|
||||
"subject": "Upgrade the MineAdmin to 3.2-alpha.1, and update Hyperf to the 3.2 release.",
|
||||
"files": [
|
||||
"composer.json",
|
||||
"config/autoload/cache.php"
|
||||
],
|
||||
"lines": 105,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pagible-admin-proxy-dns-rebinding-ssrf",
|
||||
"repo": "https://github.com/aimeos/pagible",
|
||||
"vulnerable": "03f03b2f81b20b1c1625ff227235c43791ddbb33",
|
||||
"fixed": "09a8205d513ec89ed22cdd7bdae0f4c181cee082",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"inferred_fix_changes_sites": {
|
||||
"admin/src/Controllers/AdminController.php::proxy": true,
|
||||
"core/src/Utils.php::isValidUrl": true
|
||||
},
|
||||
"sink_file": "admin/src/Controllers/AdminController.php",
|
||||
"sink_functions": [
|
||||
"proxy"
|
||||
],
|
||||
"sink_sha256_vulnerable": "cedb2973084f6ba825df0f4cf0d9edc1270c25e236155da0a1f7dddb8fa21340",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "eb6a56affa39cb0aa88e894227c2dc871f6a7dc508a8121d205cb8bb3c2105a9",
|
||||
"fix_changed_files": 3,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "981b8d63e52e7ba8875a9a52c1865eb3bc662154",
|
||||
"tip": "d7577efd68e9c5e36436c8572bfed0df2be2c634",
|
||||
"subject": "Publish related items (#210)",
|
||||
"files": [
|
||||
"core/src/Commands/Publish.php",
|
||||
"core/src/Models/Element.php",
|
||||
"core/src/Models/Page.php",
|
||||
"graphql/src/GraphQL/Mutations/PubElement.php",
|
||||
"graphql/src/GraphQL/Mutations/PubPage.php",
|
||||
"graphql/tests/GraphqlPageTest.php",
|
||||
"mcp/src/Tools/PublishElement.php",
|
||||
"mcp/src/Tools/PublishPage.php"
|
||||
],
|
||||
"lines": 44,
|
||||
"side": "after the fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wp-graphql-updatepost-publish-bypass",
|
||||
"repo": "https://github.com/wp-graphql/wp-graphql",
|
||||
"vulnerable": "d53b0e566f20a8df0664559cb6432d52a7cd285c",
|
||||
"fixed": "55441663eaa33c3f2e05de038c8286c845916461",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"plugins/wp-graphql/src/Mutation/PostObjectUpdate.php::mutate_and_get_payload"
|
||||
],
|
||||
"site_basis": {
|
||||
"plugins/wp-graphql/src/Mutation/PostObjectUpdate.php::mutate_and_get_payload": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"sink_file": "plugins/wp-graphql/src/Mutation/PostObjectUpdate.php",
|
||||
"sink_functions": [
|
||||
"mutate_and_get_payload"
|
||||
],
|
||||
"sink_sha256_vulnerable": "e39fe898c11102ea902c0db1a97cfc641c676d88d54bbb12d50c67ac9f9e4447",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "744928c8eeefdd40b1c7f48ce6ae7fb71f07ff26a95b26321725eaf4bffebdde",
|
||||
"fix_changed_files": 4,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "b583bb6ec87016965085afc7ea586f114c711c68",
|
||||
"tip": "cc12aed380414545a005048bfcebe4ba50e9c8f9",
|
||||
"subject": "fix: point acf.wpgraphql.com links at the wpgraphql.com docs portal (#4251)",
|
||||
"files": [
|
||||
"plugins/wp-graphql-acf/README.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/advanced-link.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/button-group.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/checkbox.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/code-editor.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/color-picker.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/countries.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/currencies.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/date-picker.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/date-range-picker.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/datetimepicker.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/email.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/file.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/gallery.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/google-map.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/group.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/image-selector.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/image-sizes.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/image.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/languages.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/link.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/radio-button.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/range.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/select.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/taxonomies.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/taxonomy-terms.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/taxonomy.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/text-area.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/text.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/time-picker.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/true-false.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/url.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/user-roles.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/user.md",
|
||||
"plugins/wp-graphql-acf/docs/field-types/wysiwyg.md",
|
||||
"plugins/wp-graphql-acf/readme.txt",
|
||||
"plugins/wp-graphql/docs/compatibility-guide.md",
|
||||
"plugins/wp-graphql/src/Admin/AdminNotices.php",
|
||||
"plugins/wp-graphql/src/Admin/Extensions/Registry.php"
|
||||
],
|
||||
"lines": 90,
|
||||
"side": "before the vulnerable pin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sylius-mollie-thankyou-order-idor",
|
||||
"repo": "https://github.com/Sylius/MolliePlugin",
|
||||
"vulnerable": "7400f1489a4b699df97ced9499acde4faaa476d5",
|
||||
"fixed": "01316b3ad3cf82e3c5ad160115d0a2cf89174e49",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sink_file": "src/Controller/Shop/PageRedirectController.php",
|
||||
"sink_functions": [
|
||||
"thankYouAction"
|
||||
],
|
||||
"sink_sha256_vulnerable": "60578839af598e79b4f3792ad31e8a2c80a360c53fd75609bc25acfb68c4fcd9",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "950d09980903953d802456cafed8a3efb4f9f0e953cdf49482e9ab4a9ac68de6",
|
||||
"fix_changed_files": 7,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "7cc02612b4c6b4403a2b0a1bcef1eb7c5382f505",
|
||||
"tip": "45b01569853d16d04ffd50df281e2fd607650864",
|
||||
"subject": "[UPMERGE] 3.2 -> 3.3 (#333)",
|
||||
"files": [
|
||||
"src/Client/MollieApiClient.php",
|
||||
"src/SyliusMolliePlugin.php",
|
||||
"src/Twig/Extension/MolliePluginLatestVersion.php"
|
||||
],
|
||||
"lines": 21,
|
||||
"side": "before the vulnerable pin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wp-podlove-generation-guard-poi",
|
||||
"repo": "https://github.com/podlove/podlove-publisher",
|
||||
"vulnerable": "4eedbd66796942ab052de2dbd6311a24cc75c4a7",
|
||||
"fixed": "ea878175f2612d54888d9e2aa49ae4db93b724fd",
|
||||
"problems": [],
|
||||
"vulnerable_is_ancestor_of_fixed": true,
|
||||
"sites": [
|
||||
"lib/modules/shownotes/rest_api.php::create_link_item",
|
||||
"lib/image_cache/generation_guard.php::__destruct",
|
||||
"lib/model/base.php::to_array"
|
||||
],
|
||||
"site_basis": {
|
||||
"lib/modules/shownotes/rest_api.php::create_link_item": "advisory-named",
|
||||
"lib/image_cache/generation_guard.php::__destruct": "advisory-named",
|
||||
"lib/model/base.php::to_array": "resolved-from-fix-diff"
|
||||
},
|
||||
"site_files_unresolved": {},
|
||||
"inferred_fix_changes_sites": {
|
||||
"lib/modules/shownotes/rest_api.php::create_link_item": true,
|
||||
"lib/image_cache/generation_guard.php::__destruct": false
|
||||
},
|
||||
"sink_file": "lib/modules/shownotes/rest_api.php",
|
||||
"sink_functions": [
|
||||
"create_link_item"
|
||||
],
|
||||
"sink_sha256_vulnerable": "5023f7950ad450c6f0e9ca9bec474f161a509b0b3ddbf8f9f495593738dd8a05",
|
||||
"sink_functions_missing": [],
|
||||
"sink_sha256_fixed": "316f85861d95e63a9a7cc1f089e2b6c3f1ac61aa92092e8b279534b226fe042f",
|
||||
"fix_changed_files": 9,
|
||||
"fix_touches_sink_file": true,
|
||||
"fix_touches_fix_site": false,
|
||||
"benign_proposal": {
|
||||
"base": "af187ad4bba2739839a0aa1e6cf2cabd6f9b99f5",
|
||||
"tip": "50da8db0f5a85afaf27cd6199940be0403b368ec",
|
||||
"subject": "fix PHP warning",
|
||||
"files": [
|
||||
"lib/modules/contributors/contributor_list_table.php"
|
||||
],
|
||||
"lines": 2,
|
||||
"side": "after the fix"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -78,22 +78,179 @@ def sink_parts(sink: str) -> tuple[str, list[str]]:
|
||||
return path.strip(), functions
|
||||
|
||||
|
||||
def benign(repo: Path, fixed: str, sink_file: str, extensions: tuple[str, ...]) -> dict[str, object] | None:
|
||||
PHP_FUNCTION = re.compile(r"\bfunction\s*&?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(")
|
||||
|
||||
|
||||
def php_code(text: str) -> str:
|
||||
"""`text` with comments, string contents and inline HTML blanked to spaces, newlines kept (line numbers hold).
|
||||
|
||||
Enough to find function bodies by brace matching; heredoc/nowdoc bodies are blanked too. Not a parser: a file
|
||||
that defeats it yields no function, and the site falls back to `<global>`, which the freeze record shows."""
|
||||
out, i, n, php = list(text), 0, len(text), False
|
||||
|
||||
def blank(start: int, end: int) -> None:
|
||||
for k in range(start, min(end, n)):
|
||||
if out[k] != "\n":
|
||||
out[k] = " "
|
||||
|
||||
while i < n:
|
||||
if not php:
|
||||
j = text.find("<?", i)
|
||||
blank(i, n if j < 0 else j)
|
||||
if j < 0:
|
||||
break
|
||||
php, i = True, j + (5 if text.startswith("<?php", j) else 2)
|
||||
continue
|
||||
if text.startswith("?>", i):
|
||||
php, i = False, i + 2
|
||||
elif text.startswith("//", i) or text[i] == "#" and not text.startswith("#[", i):
|
||||
j = min(k for k in (text.find("\n", i), text.find("?>", i), n) if k >= 0)
|
||||
blank(i, j)
|
||||
i = j
|
||||
elif text.startswith("/*", i):
|
||||
j = text.find("*/", i + 2)
|
||||
j = n if j < 0 else j + 2
|
||||
blank(i, j)
|
||||
i = j
|
||||
elif text.startswith("<<<", i):
|
||||
label = re.match(r"<<<\s*['\"]?([A-Za-z_][A-Za-z0-9_]*)", text[i:])
|
||||
end = re.compile(r"^\s*" + re.escape(label.group(1)) + r"\b", re.M) if label else None
|
||||
found = end.search(text, text.find("\n", i) + 1) if end else None
|
||||
j = found.end() if found else i + 3
|
||||
blank(i, j)
|
||||
i = j
|
||||
elif text[i] in "'\"":
|
||||
quote, j = text[i], i + 1
|
||||
while j < n and text[j] != quote:
|
||||
j += 2 if text[j] == "\\" else 1
|
||||
blank(i + 1, j)
|
||||
i = j + 1
|
||||
else:
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def php_functions(text: str) -> list[tuple[str, int, int]]:
|
||||
"""(name, first line, last line) of every named function or method body in a PHP file, 1-based."""
|
||||
code, spans = php_code(text), []
|
||||
for match in PHP_FUNCTION.finditer(code):
|
||||
brace, semi = code.find("{", match.end()), code.find(";", match.end())
|
||||
if brace < 0 or 0 <= semi < brace: # abstract or interface method: no body
|
||||
continue
|
||||
depth, k = 0, brace
|
||||
while k < len(code):
|
||||
depth += {"{": 1, "}": -1}.get(code[k], 0)
|
||||
if depth == 0:
|
||||
break
|
||||
k += 1
|
||||
spans.append((match.group(1), code.count("\n", 0, match.start()) + 1, code.count("\n", 0, k) + 1))
|
||||
return spans
|
||||
|
||||
|
||||
def enclosing(spans: list[tuple[str, int, int]], line: int, inserted: bool = False) -> str:
|
||||
"""The innermost named function containing `line`, or `<global>`. For a pure insertion, `line` is the line the
|
||||
new code follows, so a function whose closing brace is on that line does not contain it."""
|
||||
inside = [(last - first, name) for name, first, last in spans if first <= line <= last and not (inserted and line == last)]
|
||||
return min(inside)[1] if inside else "<global>"
|
||||
|
||||
|
||||
def old_hunks(repo: Path, vulnerable: str, fixed: str, path: str) -> list[tuple[int, int, bool]]:
|
||||
"""Vulnerable-side line ranges the fix changes in `path`, and whether each is a pure insertion (then the range
|
||||
is the line it follows, at least 1)."""
|
||||
diff = git(repo, "diff", "-U0", vulnerable, fixed, "--", path)
|
||||
ranges = []
|
||||
for start, count in re.findall(r"^@@ -(\d+)(?:,(\d+))? \+", diff, re.M):
|
||||
first, size = int(start), 1 if count == "" else int(count)
|
||||
ranges.append((max(first, 1), max(first, 1), True) if size == 0 else (first, first + size - 1, False))
|
||||
return ranges
|
||||
|
||||
|
||||
def changed_functions(repo: Path, vulnerable: str, fixed: str, path: str) -> list[str]:
|
||||
"""The functions (or `<global>`) of `path` at the vulnerable pin that the fix's hunks touch, in hunk order."""
|
||||
text = (blob(repo, vulnerable, path) or b"").decode(errors="replace")
|
||||
spans, names = php_functions(text), []
|
||||
for first, last, inserted in old_hunks(repo, vulnerable, fixed, path):
|
||||
for line in range(first, last + 1):
|
||||
name = enclosing(spans, line, inserted)
|
||||
# New code added between functions (a new helper) changes no vulnerable-side code: no site.
|
||||
if name not in names and not (inserted and name == "<global>"):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
def resolve_sites(repo: Path, case: dict[str, object], changed: list[str]) -> dict[str, object]:
|
||||
"""Decision 2 of population v3: a file the advisory names without a function gets, as its declared site, the
|
||||
function(s) the fix's diff of that file changes. Advisory-named sites are kept and marked as such."""
|
||||
declared = [str(s) for s in case.get("sites", [])] # type: ignore[union-attr]
|
||||
recorded = case.get("site_basis") or {} # a frozen population keeps each site's basis
|
||||
basis = {site: recorded.get(site, "advisory-named") for site in declared} # type: ignore[union-attr]
|
||||
unresolved = {}
|
||||
named_files = {site.partition("::")[0] for site in declared}
|
||||
for path in [str(f) for f in case.get("site_files", [])]: # type: ignore[union-attr]
|
||||
if path in named_files:
|
||||
continue
|
||||
if path not in changed:
|
||||
unresolved[path] = "the fix does not change this file"
|
||||
continue
|
||||
for name in changed_functions(repo, str(case["vulnerable"]), str(case["fixed"]), path):
|
||||
basis.setdefault(f"{path}::{name}", "resolved-from-fix-diff")
|
||||
return {"sites": list(basis), "site_basis": basis, "site_files_unresolved": unresolved}
|
||||
|
||||
|
||||
def fix_changes_sites(repo: Path, case: dict[str, object], sites: list[str]) -> dict[str, bool]:
|
||||
"""Per declared site: does the fix's diff change a line inside that function (or the file, for `<global>`)?"""
|
||||
result = {}
|
||||
for site in sites:
|
||||
path, _, name = site.partition("::")
|
||||
hunks = old_hunks(repo, str(case["vulnerable"]), str(case["fixed"]), path)
|
||||
if name == "<global>" or not hunks:
|
||||
result[site] = bool(hunks)
|
||||
continue
|
||||
spans = php_functions((blob(repo, str(case["vulnerable"]), path) or b"").decode(errors="replace"))
|
||||
result[site] = any(enclosing(spans, line, inserted) == name for first, last, inserted in hunks for line in range(first, last + 1))
|
||||
return result
|
||||
|
||||
|
||||
def benign(
|
||||
repo: Path, fixed: str, sink_file: str, extensions: tuple[str, ...], strict: bool = False, vulnerable: str = ""
|
||||
) -> dict[str, object] | None:
|
||||
"""The first acceptable change after the fix (see the module docstring).
|
||||
|
||||
`strict` (a single-language population, v3) tightens the proposal: the change is measured against its first
|
||||
parent, so a merge counts with everything it brings in, and every message it brings in is checked for security
|
||||
words (a branch-sync merge can carry the fix itself); it must change a file of the population's language; and
|
||||
when no commit after the fix qualifies, the history before the vulnerable pin is searched, newest first."""
|
||||
head = git(repo, "rev-parse", "origin/HEAD", check=False).strip() or git(repo, "rev-parse", "HEAD").strip()
|
||||
commits = git(repo, "rev-list", "--first-parent", "--reverse", f"{fixed}..{head}", check=False).split()[:400]
|
||||
span = [f"{fixed}..{head}"]
|
||||
if subprocess.run(["git", "-C", str(repo), "merge-base", "--is-ancestor", fixed, head], check=False).returncode:
|
||||
# A fix on a maintenance branch (population v3: kirby 4.x, roundcube 1.6): the default branch's commits
|
||||
# after the fix date, not its whole history outside the branch.
|
||||
span = [f"--since={git(repo, 'log', '-1', '--format=%cI', fixed).strip()}", head]
|
||||
commits = git(repo, "rev-list", "--first-parent", "--reverse", *span, check=False).split()[:400]
|
||||
after = set(commits)
|
||||
if strict and vulnerable:
|
||||
commits += git(repo, "rev-list", "--first-parent", "--max-count=400", f"{vulnerable}^", check=False).split()
|
||||
for commit in commits:
|
||||
message = git(repo, "log", "-1", "--format=%s%n%b", commit)
|
||||
parent = git(repo, "rev-parse", f"{commit}^", check=False).strip()
|
||||
if not parent:
|
||||
continue
|
||||
if strict:
|
||||
message = git(repo, "log", "--format=%s%n%b", f"{parent}..{commit}")
|
||||
stat = git(repo, "diff", "--numstat", parent, commit).splitlines()
|
||||
else:
|
||||
message = git(repo, "log", "-1", "--format=%s%n%b", commit)
|
||||
stat = git(repo, "show", "--numstat", "--format=", commit).splitlines()
|
||||
if SECURITY_WORDS.search(message):
|
||||
continue
|
||||
stat = git(repo, "show", "--numstat", "--format=", commit).splitlines()
|
||||
files = [line.split("\t") for line in stat if line.count("\t") == 2]
|
||||
if not files or any(f[2] == sink_file for f in files):
|
||||
continue
|
||||
source = [f for f in files if f[2].endswith(extensions)]
|
||||
lines = sum(int(a) + int(b) for a, b, _ in files if a.isdigit() and b.isdigit())
|
||||
if source and 0 < lines <= MAX_BENIGN_LINES:
|
||||
parent = git(repo, "rev-parse", f"{commit}^").strip()
|
||||
return {"base": parent, "tip": commit, "subject": message.splitlines()[0][:120], "files": [f[2] for f in files], "lines": lines}
|
||||
subject = git(repo, "log", "-1", "--format=%s", commit).strip()
|
||||
side = "after the fix" if commit in after else "before the vulnerable pin"
|
||||
return {"base": parent, "tip": commit, "subject": subject[:120], "files": [f[2] for f in files], "lines": lines, "side": side}
|
||||
return None
|
||||
|
||||
|
||||
@@ -112,7 +269,22 @@ def verify(case: dict[str, object], cache: Path) -> dict[str, object]:
|
||||
record["vulnerable_is_ancestor_of_fixed"] = ordered
|
||||
if not ordered:
|
||||
problems.append("the vulnerable pin is not an ancestor of the fixed pin")
|
||||
changed = git(repo, "diff", "--name-only", vulnerable, fixed).splitlines()
|
||||
sites = [str(site) for site in case.get("sites", [])] # type: ignore[union-attr]
|
||||
if case.get("site_files"):
|
||||
# Population v3: files the advisory names without a function are resolved from the fix's diff.
|
||||
record.update(resolve_sites(repo, case, changed))
|
||||
sites = record["sites"] # type: ignore[assignment]
|
||||
if not sites:
|
||||
problems.append("no declared site: the advisory names no function and the fix changes no named file")
|
||||
if case.get("fix_named_by_advisory") is False:
|
||||
# An inferred fix must change the declared sink file and at least one declared site function.
|
||||
basis = record.get("site_basis") or {}
|
||||
named = [s for s in sites if basis.get(s, "advisory-named") == "advisory-named"] # type: ignore[union-attr]
|
||||
touched = fix_changes_sites(repo, case, named)
|
||||
record["inferred_fix_changes_sites"] = touched
|
||||
if not any(touched.values()):
|
||||
problems.append("the inferred fix changes none of the declared site functions: drop the case")
|
||||
if sites:
|
||||
# Declared `path::function` sites (population v2) are what the verification reads; the first names the
|
||||
# sink file, and every function declared in that file must appear in it.
|
||||
@@ -139,7 +311,6 @@ def verify(case: dict[str, object], cache: Path) -> dict[str, object]:
|
||||
problems.append(f"none of {functions} appears in {sink_file} at the vulnerable pin")
|
||||
if after is not None:
|
||||
record["sink_sha256_fixed"] = hashlib.sha256(after).hexdigest()
|
||||
changed = git(repo, "diff", "--name-only", vulnerable, fixed).splitlines()
|
||||
record["fix_changed_files"] = len(changed)
|
||||
record["fix_touches_sink_file"] = sink_file in changed
|
||||
fix_site = str(case.get("fix_site", "")).partition("::")[0].strip()
|
||||
@@ -148,7 +319,10 @@ def verify(case: dict[str, object], cache: Path) -> dict[str, object]:
|
||||
problems.append(f"the fix changes neither {sink_file} nor a declared fix_site")
|
||||
languages = [str(case["language"]), *[str(x) for x in case.get("also", [])]] # type: ignore[union-attr]
|
||||
extensions = tuple(e for lang in languages for e in EXTENSIONS.get(lang, ()))
|
||||
record["benign_proposal"] = benign(repo, fixed, sink_file, extensions)
|
||||
strict = bool(case.get("_population_language"))
|
||||
if strict:
|
||||
extensions = EXTENSIONS.get(str(case["_population_language"]), extensions)
|
||||
record["benign_proposal"] = benign(repo, fixed, sink_file, extensions, strict, vulnerable)
|
||||
if record["benign_proposal"] is None and not case.get("benign"):
|
||||
problems.append("no benign change found automatically; choose one by hand")
|
||||
return record
|
||||
@@ -161,7 +335,11 @@ def main() -> int:
|
||||
parser.add_argument("--population", type=Path, default=POPULATION, help="a population-vN.toml in this directory")
|
||||
args = parser.parse_args()
|
||||
population = args.population if args.population.is_absolute() else HERE / args.population.name
|
||||
cases = tomllib.loads(population.read_text())["case"]
|
||||
data = tomllib.loads(population.read_text())
|
||||
cases = data["case"]
|
||||
for case in cases:
|
||||
# A single-language population (v3) gets the strict benign proposal (see `benign`).
|
||||
case["_population_language"] = data.get("language", "")
|
||||
only = {c.strip() for c in args.only.split(",") if c.strip()}
|
||||
records = []
|
||||
for case in cases:
|
||||
|
||||
@@ -44,9 +44,19 @@
|
||||
#
|
||||
# Licences marked "verify" must be checked before any result is published. Kirby is under a proprietary licence:
|
||||
# it stays in the population, but its results are unpublishable without the licensor's permission.
|
||||
#
|
||||
# FROZEN 2026-09-30 (freeze.py, freeze-v3-php.json; git history, file lists and fix diffs only). No case dropped:
|
||||
# both inferred fixes change a declared site function (pagible: proxy and isValidUrl; podlove: create_link_item,
|
||||
# while GenerationGuard::__destruct is not in the diff). Sites resolved from the fix diff are marked in
|
||||
# `site_basis`; a file the fix does not change stays unresolved (`site_files_unresolved`). Benign controls come
|
||||
# from freeze.py's strict proposal (single-language population: PHP change, merges judged with everything they
|
||||
# bring in, history before the vulnerable pin when nothing after the fix qualifies), reviewed on commit messages
|
||||
# and file names; five proposals were replaced by hand, with the reason in `how`.
|
||||
|
||||
schema_version = 1
|
||||
status = "reserved"
|
||||
status = "frozen"
|
||||
frozen_on = "2026-09-30"
|
||||
freeze_record = "freeze-v3-php.json"
|
||||
language = "php"
|
||||
reserved_on = "2026-09-30"
|
||||
selected_by = "maintainer"
|
||||
@@ -80,6 +90,7 @@ privilege = "not stated ('remotely')"
|
||||
license = "BSD-3-Clause"
|
||||
confidence = "medium"
|
||||
caveats = ["VulDB auto-generated description; the fix message ('Prevent textconv') suggests git config/attribute abuse rather than shell metacharacters, and the patched lines may not be in getDefaultBranch -- verify at freeze"]
|
||||
benign = { base = "ce5806c849f453d7200c1f488ee6aba19eef9a4c", tip = "ecc8e18e03d9b547e82b7c7c148cb00ba01c0cc9", subject = "Merge pull request #902 from zoredache/sort_repositories", how = "automatic proposal (before the vulnerable pin), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "qloapps-customermessage-bo-query-sqli"
|
||||
@@ -97,13 +108,15 @@ ancestry = "parent"
|
||||
fix_named_by_advisory = true
|
||||
sink = "database query in CustomerMessage.php built from the `bo_query` parameter"
|
||||
source = "`bo_query` request parameter"
|
||||
sites = []
|
||||
sites = ["classes/CustomerMessage.php::searchByName"]
|
||||
site_files = ["classes/CustomerMessage.php"]
|
||||
site_basis = { "classes/CustomerMessage.php::searchByName" = "resolved-from-fix-diff" }
|
||||
site_derivation = "the advisory names 'CustomerMessage.php' (basename) and the parameter; the classes/ path is from the fix commit; no function named"
|
||||
privilege = "administrator"
|
||||
license = "OSL-3.0"
|
||||
confidence = "medium"
|
||||
caveats = ["one fix covers two advisories (Address.php is GHSA-mj48); repo 129 MB, near the size cap", "function-level site to be resolved at freeze"]
|
||||
benign = { base = "5c52b864878848a625a5d9cfbb6e3925ab853ad1", tip = "a8f6071627bf97a77d715c861c1157fded5e9e60", subject = "Merge pull request #1813 from Azhar-Webkul/gli-3095", how = "chosen by hand after review: proposal e43b4500 changes payment-status logic; next passing change taken" }
|
||||
|
||||
[[case]]
|
||||
id = "roundcube-markasjunk-cmd-learn-rce"
|
||||
@@ -121,13 +134,15 @@ ancestry = "parent; ancestor of tag 1.6.18"
|
||||
fix_named_by_advisory = true
|
||||
sink = "the markasjunk plugin's cmd_learn driver runs a shell command with placeholder replacement values"
|
||||
source = "crafted placeholder replacement values (message/user data) when a message is marked as junk"
|
||||
sites = []
|
||||
sites = ["plugins/markasjunk/drivers/cmd_learn.php::_do_salearn"]
|
||||
site_files = ["plugins/markasjunk/drivers/cmd_learn.php"]
|
||||
site_basis = { "plugins/markasjunk/drivers/cmd_learn.php::_do_salearn" = "resolved-from-fix-diff" }
|
||||
site_derivation = "the advisory names 'the cmd_learn driver of the markasjunk plugin'; the file path follows from that name and is confirmed by the fix commit; no function named"
|
||||
privilege = "authenticated webmail user"
|
||||
license = "GPL-3.0"
|
||||
confidence = "medium"
|
||||
caveats = ["only instances that enable the markasjunk plugin with the cmd_learn driver"]
|
||||
benign = { base = "1cddafded973c4bf6faa173accaf8f52ca1ac0d0", tip = "747f544708ed1d698b677d333346164a9451aeda", subject = "Simplify", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "maccms10-template-blacklist-rce"
|
||||
@@ -145,13 +160,15 @@ ancestry = "parent"
|
||||
fix_named_by_advisory = true
|
||||
sink = "ThinkPHP `{if condition=...}` compiled into raw PHP; the admin template editor only blocks a function blacklist"
|
||||
source = "template content saved through the admin template editor"
|
||||
sites = []
|
||||
sites = ["application/admin/controller/Template.php::info"]
|
||||
site_files = ["application/admin/controller/Template.php"]
|
||||
site_basis = { "application/admin/controller/Template.php::info" = "resolved-from-fix-diff" }
|
||||
site_derivation = "file path quoted by the advisory; no function named"
|
||||
privilege = "administrator"
|
||||
license = "GitHub: NOASSERTION; no licence declared by the project [verify before publishing]"
|
||||
confidence = "medium"
|
||||
caveats = ["guard bypass (incomplete blacklist); the fix is still a blacklist", "licence not declared on GitHub"]
|
||||
benign = { base = "6ace264920bc50a07944c163b17a8fd8a3f7687f", tip = "c9f8d0e848489a2dc73daf1106748d2cd94d8138", subject = "修复 meilisearch 排序规则语法兼容问题", how = "chosen by hand after review: proposal 668ba418 fixes a registration-API validator (input validation); rejected" }
|
||||
|
||||
[[case]]
|
||||
id = "mantisbt-tokenizer-eval-rce"
|
||||
@@ -169,14 +186,17 @@ ancestry = "parent; ancestor of tag release-2.28.4"
|
||||
fix_named_by_advisory = true
|
||||
sink = "`Tokenizer` (reached through `ConfigParser`) calls `eval()` with a `return;` prefix; PHP hoists class declarations past the return"
|
||||
source = "non-string configuration value submitted to adm_config_set.php (Manage Configuration)"
|
||||
sites = []
|
||||
sites = ["core/classes/Tokenizer.class.php::__construct"]
|
||||
site_files = ["core/classes/Tokenizer.class.php", "adm_config_set.php"]
|
||||
site_basis = { "core/classes/Tokenizer.class.php::__construct" = "resolved-from-fix-diff" }
|
||||
site_files_unresolved = { "adm_config_set.php" = "the fix does not change this file" }
|
||||
sink_evidence = "eval("
|
||||
site_derivation = "the advisory names adm_config_set.php, ConfigParser, Tokenizer and eval(); the Tokenizer file path and the enclosing function are not named (path from the fix commit)"
|
||||
privilege = "administrator"
|
||||
license = "GitHub: none; GPL per project [verify before publishing]"
|
||||
confidence = "high"
|
||||
caveats = ["function-level site must be resolved at freeze by reading the file (the advisory names only the class)"]
|
||||
benign = { base = "088fa767bce246b6d2f083f32d937f22c8de4767", tip = "d6ca719b591f70acfa711fc41705388557b6ac04", subject = "Improve Workflow Graph display.", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "sharp-editor-iframe-srcdoc-xss"
|
||||
@@ -194,14 +214,16 @@ ancestry = "parent; ancestor of tag v9.22.5"
|
||||
fix_named_by_advisory = true
|
||||
sink = "Symfony HtmlSanitizer config allows the iframe `srcdoc` attribute; the value is rendered as live HTML"
|
||||
source = "Editor field content saved by a user with edit permission"
|
||||
sites = []
|
||||
sites = ["src/Utils/Sanitization/FormatsSanitizedValue.php::sanitizer", "src/Utils/Sanitization/FormatsSanitizedValue.php::isEncodingNeeded"]
|
||||
site_files = ["src/Utils/Sanitization/FormatsSanitizedValue.php"]
|
||||
site_basis = { "src/Utils/Sanitization/FormatsSanitizedValue.php::sanitizer" = "resolved-from-fix-diff", "src/Utils/Sanitization/FormatsSanitizedValue.php::isEncodingNeeded" = "resolved-from-fix-diff" }
|
||||
sink_evidence = "srcdoc"
|
||||
site_derivation = "file path quoted by the advisory; no function named"
|
||||
privilege = "authenticated editor"
|
||||
license = "MIT"
|
||||
confidence = "medium"
|
||||
caveats = ["the flaw is a sanitizer allow-list entry, not a missing escape call"]
|
||||
benign = { base = "ec509a22c808a5bd9dfad6a0a85c92ce6f411e21", tip = "7c601fffe930fc321a5d8bcb25cb072d50a6fc08", subject = "Update version to v9.22.5", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "privatebin-jsonld-request-uri-injection"
|
||||
@@ -226,6 +248,7 @@ privilege = "unauthenticated"
|
||||
license = "GitHub: NOASSERTION; project files state a zlib-style licence [verify before publishing]"
|
||||
confidence = "high"
|
||||
caveats = ["the fix changes Request.php only; Controller::_jsonld is a declared site outside the diff", "response type is JSON-LD, not HTML", "licence NOASSERTION on GitHub (zlib-style per project, verify)"]
|
||||
benign = { base = "75f056dcda955d94c17ec5a4f8c54a9b7bfcee07", tip = "3035d6e5d3cceb7f06f7b7bb80968727879c52b0", subject = "incrementing version", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "openstamanager-group-name-html-injection"
|
||||
@@ -251,6 +274,7 @@ privilege = "authenticated user able to create groups"
|
||||
license = "GPL-3.0"
|
||||
confidence = "high"
|
||||
caveats = ["second advisory commit (7c9eafe9, global GET/POST filtering) is not in the pinned pair"]
|
||||
benign = { base = "0b88ad1963871abfa5e134004ca0c74c496d8d2f", tip = "1832b85d0e570e0b873c7c23dcfcb6b5d1365a28", subject = "fix: ricerca riferimenti in importazione fattura per riga con articolo non esistente", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "wp-geo-my-wp-info-window-lfi"
|
||||
@@ -268,13 +292,15 @@ ancestry = "parent"
|
||||
fix_named_by_advisory = true
|
||||
sink = "include of a file chosen through `gmw_posts_locator_ajax_info_window_loader`"
|
||||
source = "AJAX request parameters (unauthenticated)"
|
||||
sites = ["plugins/posts-locator/includes/gmw-posts-locator-functions.php::gmw_posts_locator_ajax_info_window_loader"]
|
||||
sites = ["plugins/posts-locator/includes/gmw-posts-locator-functions.php::gmw_posts_locator_ajax_info_window_loader", "includes/gmw-functions.php::gmw_ajax_info_window_init"]
|
||||
site_files = ["includes/gmw-functions.php"]
|
||||
site_basis = { "plugins/posts-locator/includes/gmw-posts-locator-functions.php::gmw_posts_locator_ajax_info_window_loader" = "advisory-named", "includes/gmw-functions.php::gmw_ajax_info_window_init" = "resolved-from-fix-diff" }
|
||||
site_derivation = "function named by the advisory text; files from the advisory's own trac reference links (gmw-functions.php#L1945/L1987, gmw-posts-locator-functions.php#L964/L975); the function-to-file pairing is an assumption to confirm at freeze"
|
||||
privilege = "unauthenticated"
|
||||
license = "GitHub: none; GPL per project [verify before publishing]"
|
||||
confidence = "medium"
|
||||
caveats = ["WordPress plugin; GitHub shows no licence (plugin header GPL presumed, verify)", "release commit (small)"]
|
||||
benign = { base = "9c394482d1509ca9222365a91117a10b19619be8", tip = "85bcd85541c777b6fd8580cb1915504a2a2b84cf", subject = "Release 4.5.5.2", how = "automatic proposal (before the vulnerable pin), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "kirby-realpath-sibling-traversal"
|
||||
@@ -299,6 +325,7 @@ privilege = "unauthenticated"
|
||||
license = "Kirby licence (proprietary, GitHub NOASSERTION) -- not OSI; results unpublishable without permission"
|
||||
confidence = "medium"
|
||||
caveats = ["only deployments whose index root has a sibling directory with the same name prefix", "Kirby is under a proprietary licence (GitHub NOASSERTION); must be cleared before publishing"]
|
||||
benign = { base = "7ee5b79dd8eea6aa3d5a7eb0c5411a6631af88f1", tip = "d333470b9d9df7a48ee8d0bc2249db36aa9dd68c", subject = "style: Fix coding style", how = "chosen by hand after review: proposals 65dfca05/0901d0cb concern the content salt, 7bbefc1e updates vendored dependencies; rejected" }
|
||||
|
||||
[[case]]
|
||||
id = "mineadmin-appstore-identifier-traversal"
|
||||
@@ -323,6 +350,7 @@ privilege = "authenticated admin with app-store access (verify)"
|
||||
license = "Apache-2.0"
|
||||
confidence = "high"
|
||||
caveats = ["Hyperf framework (not Laravel/Symfony)"]
|
||||
benign = { base = "c57753e3f547683dc3e14836c3563a51ba4edaee", tip = "ff33969ef5ee378654dacab89e6f4b1928f4edd1", subject = "Fix/service logstash namespace (#663)", how = "chosen by hand after review: proposal 15deedde is a framework/dependency upgrade; rejected" }
|
||||
|
||||
[[case]]
|
||||
id = "pagible-admin-proxy-dns-rebinding-ssrf"
|
||||
@@ -338,6 +366,7 @@ vulnerable = "03f03b2f81b20b1c1625ff227235c43791ddbb33"
|
||||
fixed = "09a8205d513ec89ed22cdd7bdae0f4c181cee082"
|
||||
ancestry = "parent; ancestor of tag 0.10.4"
|
||||
fix_named_by_advisory = false
|
||||
inferred_fix_confirmed = ["admin/src/Controllers/AdminController.php::proxy", "core/src/Utils.php::isValidUrl"]
|
||||
sink = "`AdminController::proxy` → Guzzle request after `\\Aimeos\\Cms\\Utils::isValidUrl($url)`"
|
||||
source = "`url` query parameter of `/cmsproxy`"
|
||||
sites = ["admin/src/Controllers/AdminController.php::proxy", "core/src/Utils.php::isValidUrl"]
|
||||
@@ -347,6 +376,7 @@ privilege = "authenticated (proxy token)"
|
||||
license = "MIT"
|
||||
confidence = "medium"
|
||||
caveats = ["guard bypass (DNS rebinding)"]
|
||||
benign = { base = "981b8d63e52e7ba8875a9a52c1865eb3bc662154", tip = "d7577efd68e9c5e36436c8572bfed0df2be2c634", subject = "Publish related items (#210)", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "wp-graphql-updatepost-publish-bypass"
|
||||
@@ -364,13 +394,15 @@ ancestry = "parent; ancestor of tag wp-graphql/v2.22.2"
|
||||
fix_named_by_advisory = true
|
||||
sink = "`updatePost` mutation checks only `$post_type_object->cap->edit_posts`"
|
||||
source = "GraphQL `updatePost` mutation input (status PUBLISH, published post id)"
|
||||
sites = []
|
||||
sites = ["plugins/wp-graphql/src/Mutation/PostObjectUpdate.php::mutate_and_get_payload"]
|
||||
site_files = ["plugins/wp-graphql/src/Mutation/PostObjectUpdate.php"]
|
||||
site_basis = { "plugins/wp-graphql/src/Mutation/PostObjectUpdate.php::mutate_and_get_payload" = "resolved-from-fix-diff" }
|
||||
site_derivation = "the advisory names src/Mutation/PostObjectUpdate.php; the repository is a monorepo (plugins/wp-graphql/ prefix from the fix commit); no function named"
|
||||
privilege = "Contributor"
|
||||
license = "GPL-3.0"
|
||||
confidence = "medium"
|
||||
caveats = ["fix commit also carries an unrelated Model.php change", "function-level site to resolve at freeze"]
|
||||
benign = { base = "b583bb6ec87016965085afc7ea586f114c711c68", tip = "cc12aed380414545a005048bfcebe4ba50e9c8f9", subject = "fix: point acf.wpgraphql.com links at the wpgraphql.com docs portal (#4251)", how = "automatic proposal (before the vulnerable pin), reviewed" }
|
||||
|
||||
[[case]]
|
||||
id = "sylius-mollie-thankyou-order-idor"
|
||||
@@ -395,6 +427,7 @@ privilege = "unauthenticated"
|
||||
license = "MIT"
|
||||
confidence = "high"
|
||||
caveats = ["merge commit; one fix covers two advisories (webhook forgery GHSA-rc52 is the other)"]
|
||||
benign = { base = "19f22cd673fde2b6ac750326fecbfe5fc4c06b9e", tip = "91254cc9c9821acbb3c785918759feedd827bdb9", subject = "Add support for Swish gateway (#324)", how = "chosen by hand after review: proposal 45b01569 is a branch upmerge; rejected" }
|
||||
|
||||
[[case]]
|
||||
id = "wp-podlove-generation-guard-poi"
|
||||
@@ -410,12 +443,15 @@ vulnerable = "4eedbd66796942ab052de2dbd6311a24cc75c4a7"
|
||||
fixed = "ea878175f2612d54888d9e2aa49ae4db93b724fd"
|
||||
ancestry = "linear chain of 3 commits (4eedbd66 → d49c4d7f → b59d7962 → ea878175); ancestor of tag 4.5.4"
|
||||
fix_named_by_advisory = false
|
||||
inferred_fix_confirmed = ["lib/modules/shownotes/rest_api.php::create_link_item"]
|
||||
sink = "unserialize() of model data (lib/model/base.php#L552) instantiating `Podlove\\ImageCache\\GenerationGuard`, whose `__destruct()` calls `wp_delete_file()`"
|
||||
source = "shownotes REST input handled by `create_link_item` (contributor+)"
|
||||
sites = ["lib/modules/shownotes/rest_api.php::create_link_item", "lib/image_cache/generation_guard.php::__destruct"]
|
||||
sites = ["lib/modules/shownotes/rest_api.php::create_link_item", "lib/image_cache/generation_guard.php::__destruct", "lib/model/base.php::to_array"]
|
||||
site_files = ["lib/model/base.php"]
|
||||
site_basis = { "lib/modules/shownotes/rest_api.php::create_link_item" = "advisory-named", "lib/image_cache/generation_guard.php::__destruct" = "advisory-named", "lib/model/base.php::to_array" = "resolved-from-fix-diff" }
|
||||
site_derivation = "create_link_item and GenerationGuard::__destruct named by the advisory; files from the advisory's own trac reference links"
|
||||
privilege = "Contributor"
|
||||
license = "MIT"
|
||||
confidence = "medium"
|
||||
caveats = ["fix inferred from file history (three commits)", "the advisory text describes the impact (file deletion) under CWE-502"]
|
||||
benign = { base = "af187ad4bba2739839a0aa1e6cf2cabd6f9b99f5", tip = "50da8db0f5a85afaf27cd6199940be0403b368ec", subject = "fix PHP warning", how = "automatic proposal (after the fix), reviewed" }
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""freeze.py resolves an advisory's file-only site to the function the fix changes (population v3, decision 2).
|
||||
|
||||
The resolver reads PHP text only; it must not be fooled by braces in strings, comments or inline HTML, and a line
|
||||
outside every function is `<global>`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location("freeze", Path(__file__).resolve().parents[1] / "benchmarks" / "independent" / "freeze.py")
|
||||
freeze = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(freeze)
|
||||
|
||||
PHP = """<html>{ not code }</html>
|
||||
<?php
|
||||
// a comment with { brace
|
||||
class Box
|
||||
{
|
||||
public function open($x)
|
||||
{
|
||||
$s = "}{ in a string";
|
||||
return function ($y) { return $y; };
|
||||
}
|
||||
|
||||
abstract protected function shape();
|
||||
|
||||
private static function &close() {
|
||||
/* } */
|
||||
return <<<EOT
|
||||
} heredoc {
|
||||
EOT;
|
||||
}
|
||||
}
|
||||
$top = 1;
|
||||
?>
|
||||
<div>{</div>
|
||||
<?php function helper() { return '#{'; }
|
||||
"""
|
||||
|
||||
|
||||
def test_named_functions_and_their_lines() -> None:
|
||||
spans = {name: (first, last) for name, first, last in freeze.php_functions(PHP)}
|
||||
assert spans == {"open": (6, 10), "close": (14, 19), "helper": (24, 24)}
|
||||
|
||||
|
||||
def test_a_line_resolves_to_its_innermost_named_function_or_global() -> None:
|
||||
spans = freeze.php_functions(PHP)
|
||||
assert freeze.enclosing(spans, 9) == "open" # inside the closure: the named function owns it
|
||||
assert freeze.enclosing(spans, 17) == "close"
|
||||
assert freeze.enclosing(spans, 21) == "<global>"
|
||||
assert freeze.enclosing(spans, 1) == "<global>"
|
||||
|
||||
|
||||
def test_an_insertion_after_a_closing_brace_is_outside_the_function() -> None:
|
||||
spans = freeze.php_functions(PHP)
|
||||
assert freeze.enclosing(spans, 10) == "open" # a changed line on the closing brace
|
||||
assert freeze.enclosing(spans, 10, inserted=True) == "<global>" # new code that follows it
|
||||
assert freeze.enclosing(spans, 8, inserted=True) == "open"
|
||||
Reference in New Issue
Block a user