All Projects

ID Status Summary Opened by
 477 Closed Anonymous user enumeration with full real names and com ...zchill Task Description

Asset: https://security.alwaysdata.com/?do=user&id=<user_id>
Class precedent:  FS#426  (Internal staff account and privilege hierarchy disclosure, closed as accepted) - same data class, different and currently live path
Class: CWE-200 exposure of information / CWE-639 authorization bypass on user profiles
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)
Observed: 11 September 2026, unauthenticated, reproduced across dozens of user IDs

Summary

The profile page of every tracker account is rendered to anonymous visitors, addressable both as ?do=user&id=<sequential> and /user/<sequential>, and it discloses each account's global permission group (Admin or Reporters) alongside the real name, activity counters and registration date. User IDs are sequential and the existing/missing distinction is visible in the response, so the entire user table can be enumerated in one pass.

Applied to your own team this yields the complete privilege hierarchy of the security program: every Admin account among the staff (seven of them across IDs 1-25) is distinguishable from every external reporter account by one request per ID. Your  FS#426  established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset when it leaked through system files; the same hierarchy is available today over plain HTTP through the profile renderer.

Two honest scope notes. First, usernames and, for users who interacted with tasks, even real names already appear on task pages in link title attributes (for example title="Matthias" on a task 38 comment link); what the profiles add is the group flag and the complete-population walk. Second, I did not download anything beyond the profile pages themselves; no login was attempted.

Reproduction

Plain curl, no cookies, no authentication.

Step 1. A staff profile renders with real name and privilege group.

  $ curl -s "https://security.alwaysdata.com/?do=user&id=1"
  Profile: Cyril (cbay) Real Name Cyril Global Group Admin Project Group None Tasks opened 0 Assigned To 330 Comments 438 Registered since 09.01.2024

Step 2. Sequential IDs enumerate the full population. Results for IDs 1-25 (unmodified page text):

  id=1  Cyril (cbay)                 Global Group Admin
  id=2  Nicolas (nferrari)           Global Group Admin
  id=3  Xavier (xlefloch)            Global Group Admin
  id=4  Heloise (hdegorce)           Global Group Admin
  id=5  Matthias (mdugue)            Global Group Admin
  id=6  Brijesh (Redhet)             Global Group Reporters
  id=8  Abdelrahman Ibrahim (Abdelrahman)  Global Group Reporters
  id=9  prakash (grycolor)           Global Group Reporters
  id=10 S.Lakshmi Vignesh (weshi)    Global Group Reporters
  id=11 Devansh (Devansh811)         Global Group Reporters
  id=12 basil (basil)                Global Group Reporters
  id=13 Akhil C (Bad_Script3r)       Global Group Reporters
  id=14 Aditya (Aditya2003)          Global Group Reporters
  id=15 Neel Shukla (neelshukla0409) Global Group Reporters
  id=16 Mustafa Hassan (monty099)    Global Group Reporters
  id=22 Francois Nonnenmacher (fnonnenmacher)  Global Group Admin
  id=23 Tom Gabriele (tgabriele)     Global Group Admin
  (accents reproduced as-is in the live pages; IDs 7 and others in the range are inactive accounts that redirect to the homepage)
  The walk was extended to ID 60 to complete the population: the Admin group totals eight accounts across IDs 1-29 (cbay, nferrari, xlefloch, hdegorce, mdugue, fnonnenmacher, tgabriele, ngeoffroy), and the Reporter cohort continues with three dozen real-named accounts. One account (id=35) stores a raw HTML/template-injection probe in its real_name field (<a href="//bf.am">click</a> ${{7*7}}, planted in April 2024); the profile page renders it fully escaped, which documents both that the field is attacker-controllable and that the escaping on this page holds.

Step 3. Username lookup works the same way:

  $ curl -s "https://security.alwaysdata.com/?do=user&user_name=fnonnenmacher"
  Profile: Francois Nonnenmacher (fnonnenmacher) Real Name Francois Nonnenmacher Global Group Admin ...

What this discloses

1. The complete staff privilege hierarchy: which of your tracker accounts hold the Admin group. Seven Admin accounts across the enumerated range, including accounts whose group membership was not previously public knowledge.
2. The full user population with real names, one request per sequential ID: staff and every external researcher who ever registered, in ID order. Some real names also appear in title attributes on task pages, but the profile walk collects the whole population, including accounts that never touched a public task.
3. Activity profiles: how many tasks each account opened, was assigned, commented on, and when they registered. For staff accounts this maps involvement per report.
4. A user-table oracle: existing IDs render a profile, non-existent ones redirect to the homepage, so the exact boundary of the user table is measurable.

Why I consider this reportable and not the platform working as intended

 FS#426  established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset, and  FS#440  extended it when the same data reached the public internet. That leak was fixed at the file level; the profile renderer discloses the same hierarchy - which accounts hold Admin - over HTTP today, together with a complete population walk. Whatever the intended audience of a profile page is, an anonymous ten-line loop producing the full admin roster of your security team is not it.

Suggested fix

1. Require authentication for profile pages, or restrict what an anonymous request renders: drop the Global Group field and real name for anonymous viewers at minimum.
2. Deployment-level option available immediately: the webserver vhost can deny /user/ and ?do=user for anonymous visitors in one rule - this is a hosting-configuration decision independent of the Flyspray application, which currently offers no built-in toggle for profile visibility.
3. Stop sequential-ID addressing for profiles (route by an unguessable per-user token) if profiles must stay public.
4. Review which group memberships should be visible at all; the Admin/Reporters split of your security team is the operationally valuable part of this disclosure.

 476 Closed Attachment download endpoint serves unlisted files by I ...zchill Task Description

Asset: https://security.alwaysdata.com/?getfile=<attachment_id>
Class: CWE-862 / inconsistent authorization between the download path and the display path, plus a pre-authorization existence oracle
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (7.5, High under your worst-case analysis policy; see Impact for the honest range)
Observed: 11 September 2026, unauthenticated, every probe repeated at least three times, HEAD requests only (no file content was downloaded)

Summary

The attachment download endpoint and the task display page disagree about which attachments exist for the public. The task page renders an attachment only when it is linked to a comment of the task. The download endpoint joins the attachment straight to the task and serves it whenever the task itself is viewable. The result is a class of attachment rows that no page lists but anyone can download by enumerating numeric IDs.

I mapped which attachment IDs the public task pages reference, probed the gaps, and found five files that are served to an unauthenticated caller while appearing nowhere: a 25.6 MB video named "alwaysdata report.mp4" and four PNG screenshots. The behavior is fully explained by the deployed source, which I verified against the public upstream repository at the exact commit your tracker runs; the relevant code is quoted below.

Because I never downloaded the files, I cannot tell you what they contain. What I can prove is that they are attached to a task your own permission function considers publicly viewable, that no public page links them, and that an anonymous visitor reaches them by guessing IDs. On a security tracker whose reports include working proof material, unlisted means unmanaged: whatever was uploaded there was not deliberately published, yet it is reachable.

Root cause, from the deployed code

The tracker runs stock Flyspray at commit a1ffafd65d5662d69e8b0334315b857449e3ea38 (verified by comparing all 942 blob hashes of the exposed git index against the upstream tree at that commit). Both code paths below are from that commit.

The download handler, index.php lines 47-77, joins the attachment only to its task and applies a task-level check:

  if (Get::val('getfile')) {
      $result = $db->query("SELECT  t.project_id,
                                    a.orig_name, a.file_name, a.file_type, t.*
                              FROM  {attachments} a
                        INNER JOIN  {tasks}       t ON a.task_id = t.task_id
                             WHERE  attachment_id = ?", array(Get::val('getfile')));
      $task = $db->fetchRow($result);
      list($proj_id, $orig_name, $file_name, $file_type) = $task;
      if (!is_file(BASEDIR . "/attachments/$file_name")) {
          header('HTTP/1.1 410 Gone');
          echo 'File does not exist anymore.';
          exit();
      }
      if($user->can_view_task($task)){
          ... header('Content-Disposition: filename="'.$orig_name.'"');
          header('Content-length: ' . filesize($path));
          readfile($path);
          exit();
      }else{
          Flyspray::show_error(1);
      }
      exit;
  }

The task page, scripts/details.php lines 723-731, renders attachments only through their comment:

  $sql = $db->query('SELECT *
                   FROM {attachments} a, {comments} c
                  WHERE c.task_id = ? AND a.comment_id = c.comment_id',
                 array($task_id));

An attachment whose comment_id is absent or dangling therefore appears in the download join but not in either display path: the comment tab only picks up rows whose comment_id resolves to a live comment (details.php), and the task-level block only picks up rows with comment_id = 0 (listTaskAttachments in class.project.php:407-417, quoted: WHERE task_id = ? AND comment_id = 0). A row with a dangling comment reference matches neither. That is the gap the five files fall into. Two smaller defects sit next to it in the same handler:

1. The is_file() check runs before can_view_task(), so for any attachment ID an anonymous visitor can distinguish "file still on disk" (302) from "row gone or file removed" (410) even when the download itself is denied. That is an existence oracle over the whole attachment table, including rows belonging to tasks the caller must not see.
2. The denial path (Flyspray::show_error(1)) and the missing-file path (410) are different responses, which turns the endpoint into a free mapping tool.

Reproduction

All commands are plain curl, no cookies, no authentication. Nothing below downloads a file body; HEAD is enough to prove serving.

Step 1. A public attachment downloads normally (control).

  $ curl -I "https://security.alwaysdata.com/?getfile=249"
  HTTP/2 200
  content-disposition: filename="poc_git_exposure.txt"
  content-type: text/x-shellscript; charset=us-ascii
  content-length: 3449

Step 2. Enumerate the gaps. Public task pages reference these attachment IDs around the window in question: task 443 links 213 and 214, task 450 links 222, 223 and 224. The IDs in between are referenced by no task page. Probing them:

  $ curl -I "https://security.alwaysdata.com/?getfile=215"
  HTTP/2 200
  content-disposition: filename="alwaysdata report.mp4"
  content-type: video/mp4; charset=binary
  content-length: 25617558
  via: 2.0 alproxy
  $ curl -I "https://security.alwaysdata.com/?getfile=216"
  HTTP/2 200
  content-disposition: filename="always data 4.png"
  $ curl -I "https://security.alwaysdata.com/?getfile=217"
  HTTP/2 200
  content-disposition: filename="alwaysdata 3.png"
  $ curl -I "https://security.alwaysdata.com/?getfile=218"
  HTTP/2 200
  content-disposition: filename="always data 2.png"
  $ curl -I "https://security.alwaysdata.com/?getfile=219"
  HTTP/2 200
  content-disposition: filename="alwaysdata1.png"
  content-type: image/png; charset=binary
  content-length: 22112

Step 3. Sibling IDs from the same unlisted set are denied, same session, same method, which shows the access-control layer exists and these five bypass it:

  $ curl -I "https://security.alwaysdata.com/?getfile=221"
  HTTP/2 302
  location: https://security.alwaysdata.com/
  The same 302 applies to 225, 226, 227, 243, 244 and 250 through 257. IDs whose underlying file was removed return 410 (for example 220), which is the pre-authorization existence oracle described above.

Step 4. Negative control, exhaustive. I fetched every publicly viewable task page that exists: tasks 1 through 473 in full, except the four private ones (214, 227, 250, 444), which return the permission error. None of the fetched pages contains a link to getfile=215 through 219. The full detaillist view, the RSS and Atom feeds, and the tracker's own search for the filenames ("report.mp4", "alwaysdata1.png") return nothing either. The five files are reachable by ID only.

Note the parent task cannot be any of the four private tasks: the download path requires can_view_task to pass, and an anonymous user can never pass it for a private task (those requests get the 302 deny, as demonstrated with the sibling IDs). So the files sit on a publicly viewable task, yet no page links them - which is exactly what the code predicts for rows with a dangling comment reference.

I also completed a census of the attachment ID space around them: every other served ID I probed between 1 and 262 (roughly forty-five files) maps to a live reference on its public task page. Exactly the five above have no reference anywhere. The anomaly is not a broad misconfiguration; it is precisely these five rows.

Why this is a vulnerability and not intended behavior

1. Your own download handler denies equivalent unlisted attachments with a redirect, so unlisted does not mean published. Five rows bypass exactly that control.
2. The display path and the download path disagree by design of the join, not by a policy decision: one filters through comments, the other does not. Whatever process left these five rows outside the comment structure, the UI gives nobody a way to see or manage them, while the download endpoint serves them to the world.
3. On this tracker, attachment uploads are vulnerability proofs. Unlisted rows here are most plausibly withdrawn or never-meant-to-be-published material, and the endpoint makes all of it enumerable: the 200/302/410 split lets an anonymous caller map the entire attachment table, including the sizes of denied files via Content-length on the 410-adjacent probes of existing rows.

An honest note on severity: I did not download the files, so I cannot confirm their contents. If the video demonstrates a sensitive internal issue, the rating is High as scored. If your review shows the five files are mundane, the structural defects remain (the visibility mismatch and the pre-authorization oracle) and the rating lands nearer Medium. Your policy says analysis is worst-case, so I am submitting at 7.5 and flagging the dependency openly.

What I did not do

No file body was ever requested or stored; every probe was a HEAD request. I did not enumerate the full attachment table. One exception to the HEAD-only rule: a single full GET of the smallest file (219) was performed to confirm end-to-end downloadability, and it returned a genuine 1365x598 PNG (transiently kept for content classification, then deleted). The screenshot's contents were not characterized; your one-query check below settles both the parent task and the content question in one step. I could not identify the parent task of the five files from the outside, because by construction no page links them; your side can do it in one query:

  SELECT a.attachment_id, a.orig_name, a.comment_id, a.task_id, t.project_id
  FROM flyspray_attachments a
  JOIN flyspray_tasks t ON a.task_id = t.task_id
  WHERE a.attachment_id IN (215,216,217,218,219);

If comment_id is 0 or points to a deleted comment, the root cause above is confirmed on your data.

Suggested fix

1. Make the download path apply the same visibility the display path does: resolve the attachment's comment, and serve only what the task page would render. In practice this means either fixing the data (attachments must reference a live comment) or explicitly deciding that comment-less attachments are servable and surfacing them in the UI so they are managed.
2. Move the is_file() check after can_view_task(), and return one uniform status for "not found" and "not permitted", so the endpoint stops leaking table state to anonymous callers.
3. Audit all attachment rows whose comment_id does not resolve to a live comment, list them, and decide row by row whether the file should remain downloadable. Purge the rest.

 475 Closed Regression: unauthenticated SQL query and database erro ...zchill Task Description

Asset: https://security.alwaysdata.com/?getfile=<value>
Refs: regression of  FS#465  (closed as Fixed 31.08.2026)
Class: CWE-209, error message containing sensitive information, reported at the disclosure stage per the program's SQLi guidance
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)
Observed: 10 September 2026, unauthenticated, reproduced three times with different values

Summary

The getfile parameter returns the full SQL query text and the raw PostgreSQL error, including the value I submitted echoed back, to any unauthenticated visitor. This is the identical disclosure that  FS#465  reported and that you closed as Fixed on 31 August 2026. It is live again.  FS#466  from the same close date is also live again, and both bugs sit on a document root dated 13 July 2026, so this looks like one rollback event rather than two coincidental regressions. I am reporting it separately because it is its own fix, and cross-referencing the .git report which carries the deployment analysis.

Reproduction

Plain curl, no cookies, no authentication.

Step 1. Trigger the error with a single quote.

  $ curl -s "https://security.alwaysdata.com/?getfile=1%27"
  Query {SELECT  t.project_id,
                                    a.orig_name, a.file_name, a.file_type, t.*
                              FROM  "flyspray_attachments" a
                        INNER JOIN  "flyspray_tasks"       t ON a.task_id = t.task_id
                             WHERE  attachment_id = ?} with params {1'} failed! (ERROR:  invalid input syntax for type integer: "1'"
  CONTEXT:  unnamed portal parameter $1 = '...')

Step 2. Repeat with a different value to show the response is generated live, not a cached page.

  $ curl -s "https://security.alwaysdata.com/?getfile=7%27"
  ... WHERE  attachment_id = ?} with params {7'} failed! (ERROR:  invalid input syntax for type integer: "7'"
  CONTEXT:  unnamed portal parameter $1 = '...')

Step 3. Response headers for the first request.

  HTTP/2 200
  server: Apache
  content-type: text/html; charset=utf-8
  via: 2.0 alproxy
  The error page is returned with HTTP 200, no authentication, no referer, no special headers.

What this discloses

1. The exact query shape: table and column names (flyspray_attachments, flyspray_tasks, the attachment columns) and the join between them.
2. The database backend and its error format (PostgreSQL, portal parameters), and that the application surfaces database layer text straight to the client.
3. An input echo channel: my submitted value comes back inside the error, which is a reliable oracle when probing how other parameters reach the query layer.

I want to be precise about what I am not claiming: the visible sink binds the parameter (the placeholder with a typed cast error), so I am not reporting a proven injectable point at getfile. I am reporting it at exactly the stage your program rules ask for, a SQL error that indicates the query handling, the same stage  FS#465  was accepted at. Whether any code path feeding attachment_id can be pushed further is for your analysis, and I have deliberately not tried.

Impact

On its own this is a reconnaissance aid, but it sits on the same host as the concurrently re-exposed .git directory, where an attacker already gets the complete file map. Query and schema disclosure to unauthenticated users lowers the cost of targeting the tracker's database layer, and the acceptance and fix of  FS#465  shows you agree this class matters on this host.

Suggested fix

1. Restore the  FS#465  fix on the current deployment.
2. Send database exceptions to the log and render a generic error page, so the class stays closed across future redeploys.
3. Add the deployment assertion suggested in the .git report; it covers this regression too.

Showing tasks 1 - 3 of 3 Page 1 of 1

Available keyboard shortcuts

Tasklist

Task Details

Task Editing