Summary
During a security review, I identified a critical authorization bypass in ZenTao's permission system. Any authenticated user (including guest accounts and users with minimal permissions) can access ALL controller methods containing "ajax" in their name, bypassing the group-based ACL system entirely.
Root Cause
In module/common/model.php, line 456, the isOpenMethod() function contains:
if(stripos($method, 'ajax') !== false) return true;
This blanket check runs BEFORE any group/role-based permission verification (hasPriv()). If it returns true, the entire ACL system is bypassed. This affects approximately 338 out of 360 ajax-prefixed methods across 60+ modules, since only ~22 methods implement their own secondary permission checks.
Impact
The bypass exposes sensitive functionality including:
-
Database credential disclosure — system.ajaxDBAuthUrl returns full database credentials (host, port, username, password) as a URL. No admin check.
-
Task/Document/Story data leaks — task.ajaxGetByID, doc.ajaxGetDoc, story.ajaxGetDetail return full records via raw SELECT * without permission checks, while their non-ajax counterparts (task.view, doc.view, story.view) correctly check project/product access.
-
CI/CD job execution — mr.ajaxExecJob triggers pipeline jobs without permission checks.
-
Database table modification — admin.ajaxChangeTableEngine runs ALTER TABLE without admin checks.
-
Webhook secret disclosure — webhook.ajaxGetFeishuDeptList exposes Feishu/Lark API credentials.
-
AI token disclosure — zai.ajaxGetToken returns AI integration tokens.
-
Kanban card manipulation — kanban.ajaxMoveCard directly updates story status/stage.
Contrast with Secure Code
Some ajax methods DO implement their own checks (showing developers are aware this is needed):
sso.ajaxSetConfig — checks $this->app->user->admin
instance.ajaxUninstall — checks hasPriv('space', 'browse')
instance.ajaxStop / ajaxStart — checks privileges
But the vast majority (~94%) do not, relying on the framework-level ACL that line 456 bypasses.
Recommended Fix
Primary: Remove line 456 from isOpenMethod():
- if(stripos($method, 'ajax') !== false) return true;
Then add genuinely universal ajax methods (UI helpers like misc.ajaxIgnoreBrowser, tutorial.ajaxSetTasks) to the $config->logonMethods whitelist.
Secondary (defense-in-depth): Add explicit admin/privilege checks to high-sensitivity methods like system.ajaxDBAuthUrl, admin.ajaxChangeTableEngine, and mr.ajaxExecJob, regardless of the framework fix.
Disclosure
This report is submitted in good faith to help improve the security of ZenTao. I am available to provide additional details or clarification.
Summary
During a security review, I identified a critical authorization bypass in ZenTao's permission system. Any authenticated user (including guest accounts and users with minimal permissions) can access ALL controller methods containing "ajax" in their name, bypassing the group-based ACL system entirely.
Root Cause
In
module/common/model.php, line 456, theisOpenMethod()function contains:This blanket check runs BEFORE any group/role-based permission verification (
hasPriv()). If it returnstrue, the entire ACL system is bypassed. This affects approximately 338 out of 360 ajax-prefixed methods across 60+ modules, since only ~22 methods implement their own secondary permission checks.Impact
The bypass exposes sensitive functionality including:
Database credential disclosure —
system.ajaxDBAuthUrlreturns full database credentials (host, port, username, password) as a URL. No admin check.Task/Document/Story data leaks —
task.ajaxGetByID,doc.ajaxGetDoc,story.ajaxGetDetailreturn full records via rawSELECT *without permission checks, while their non-ajax counterparts (task.view,doc.view,story.view) correctly check project/product access.CI/CD job execution —
mr.ajaxExecJobtriggers pipeline jobs without permission checks.Database table modification —
admin.ajaxChangeTableEnginerunsALTER TABLEwithout admin checks.Webhook secret disclosure —
webhook.ajaxGetFeishuDeptListexposes Feishu/Lark API credentials.AI token disclosure —
zai.ajaxGetTokenreturns AI integration tokens.Kanban card manipulation —
kanban.ajaxMoveCarddirectly updates story status/stage.Contrast with Secure Code
Some ajax methods DO implement their own checks (showing developers are aware this is needed):
sso.ajaxSetConfig— checks$this->app->user->admininstance.ajaxUninstall— checkshasPriv('space', 'browse')instance.ajaxStop/ajaxStart— checks privilegesBut the vast majority (~94%) do not, relying on the framework-level ACL that line 456 bypasses.
Recommended Fix
Primary: Remove line 456 from
isOpenMethod():- if(stripos($method, 'ajax') !== false) return true;Then add genuinely universal ajax methods (UI helpers like
misc.ajaxIgnoreBrowser,tutorial.ajaxSetTasks) to the$config->logonMethodswhitelist.Secondary (defense-in-depth): Add explicit admin/privilege checks to high-sensitivity methods like
system.ajaxDBAuthUrl,admin.ajaxChangeTableEngine, andmr.ajaxExecJob, regardless of the framework fix.Disclosure
This report is submitted in good faith to help improve the security of ZenTao. I am available to provide additional details or clarification.