-
Notifications
You must be signed in to change notification settings - Fork 15
Issue 74 Cannot Change Default Password
A brand-new installation insists you replace the seeded password before it will let you do anything else. On the page where you do that, the Change Password button did nothing at all, and the browser console filled with 404.
Reported in issue #74 by tjedelhauser.
Fixed in 9d119e17, released as update #1124.
The gate itself is described in Security; the addresses involved are described in Root Folder Tidy.
You install FreeITSM, sign in as admin for the first time, and are sent to a page headed Password Expired:
Your password has expired and must be changed before you can continue.
The form renders correctly. The fields accept input. Pressing Change Password does nothing whatsoever β no error, no success message, no spinner that finishes. Only the browser console has anything to say, and it says the same thing once per attempt:
Failed to load resource: the server responded with a status of 404 ()
api/myaccount/change_password.php
The reporter did the diagnosis themselves and put the answer in the issue: the endpoint is at /api/myaccount/change_password.php, and the page was not asking for it there.
Two things make this worse than an ordinary broken button.
- It is the very first thing a new installation asks you to do. The one install this stops dead is the one nobody has used yet.
-
You cannot go anywhere else. The gate that sends you to this page (
includes/password_gate.php) is enforced on every request, deliberately β so typing another address just sends you back. The only working control on the page was Logout instead.
The page asked the server for its endpoint like this:
const resp = await fetch('api/myaccount/change_password.php', { ... })That address has no leading slash and no application root, so the browser resolves it against whatever address the page itself was reached at. The page lives at auth/force_password_change.php and answers on two addresses:
| The page is reached at |
api/myaccount/change_password.php resolves to |
|
|---|---|---|
/force_password_change.php (the root rewrite) |
/api/myaccount/change_password.php |
β exists |
/auth/force_password_change.php (the real file) |
/auth/api/myaccount/change_password.php |
β 404 |
Both addresses serve the identical file. Only one of them makes the fetch resolve. So the page worked for as long as everything sent people to the first address β and nothing announced that this was load-bearing.
Issue #68 was the report that FreeITSM sent signed-out visitors to a login page that did not exist, because the redirects named an address that only Apache's rewrite rules created. The fix was to make the application name the real file everywhere, so that its own redirects resolve on any web server with no rewriting at all. That was right, and it stays.
But the sweep changed the redirects and left the pages alone:
// auth/login.php, after #68
header('Location: ' . BASE_URL . 'auth/force_password_change.php');From that commit on, everyone arriving at the password page arrived by the second address in the table β the one where the fetch does not resolve. The fix for one direction of this fault created the other direction of it.
The dates are close enough to be worth stating plainly: 1057b0ed landed on 16 August 2026 and issue #74 was opened on 17 August 2026. This was not an old installation; the reporter was running a build about a day old.
Every page in auth/ built its endpoint address the same way, and every page in auth/ is reachable at both depths. Three more instances were sitting next to the reported one.
| Page | Endpoint it asked for | State |
|---|---|---|
force_password_change.php |
api/myaccount/change_password.php |
Broken β the reported fault |
login.php |
api/myaccount/verify_login_otp.php |
Broken, and unreported |
forgot-password.php |
api/auth/request_password_reset.php |
Broken |
reset-password.php |
api/auth/reset_password.php |
Working, by luck |
The login one deserves attention. That fetch is the one-time-code step of signing in. Since #68 every redirect in the application points at auth/login.php, and the home page sends you there too β so on any installation with two-factor authentication switched on, nobody could complete a sign-in. That is a larger fault than the one that was reported, and it had not been reported at all, which is worth sitting with: it means the reported bug was the one somebody happened to hit first, not the worst one present.
Why reset-password.php escaped. The link people actually use is the one emailed to them, and api/auth/request_password_reset.php builds that link against the root address. So the page was reached at the depth where its relative fetch happened to resolve. It was never correct β it was standing in the right place.
forgot-password.php and reset-password.php load no includes at all. No config.php, no functions.php. Which means BASE_URL β the constant that says where the application is installed β was never defined on either of them.
Both pages already used it:
header('Location: ' . (defined('BASE_URL') ? BASE_URL : '/') . 'index.php');The defined() guard is doing real work there, and always losing. On an installation served from a subfolder, a signed-in visitor who opened the forgot-password page was redirected to /index.php β outside the application entirely. The guard turned a fatal error into a wrong answer, which is why nobody noticed.
Every one of the four now names the application root:
const resp = await fetch('<?php echo defined('BASE_URL') ? BASE_URL : '/'; ?>api/myaccount/change_password.php', { ... })and the two pages that had no way of knowing the root now load the file that defines it:
require_once __DIR__ . '/../config.php';Why BASE_URL is the right answer rather than another relative path. It is derived from where the files sit on disk relative to the web server's document root (config.php), not from the address the visitor arrived on:
$__appRoot = str_replace('\\', '/', realpath(__DIR__));
$__docRoot = str_replace('\\', '/', realpath($_SERVER['DOCUMENT_ROOT'] ?? ''));So it is identical whether the page was reached as /login or as /auth/login.php. A relative path can only ever be correct for one of the two addresses a page answers on; a filesystem-derived root is correct for both. That is the whole distinction this bug turns on.
The Logout instead link was made absolute in the same pass. It happened to work at both depths β a real file at one, a rewrite at the other β but it was working for two unrelated reasons rather than one good one, and the rewrite half is Apache and nginx configuration that an unusual deployment may not have.
| File | What was wrong |
|---|---|
auth/force_password_change.php |
The reported page. Relative fetch, and a relative logout link |
auth/login.php |
Relative fetch for the one-time-code step β MFA sign-in could not complete |
auth/forgot-password.php |
Relative fetch, and no config.php, so BASE_URL was undefined |
auth/reset-password.php |
Relative fetch, and no config.php. Working only because the emailed link arrives at the other depth |
| File | Role |
|---|---|
config.php |
Defines BASE_URL from the filesystem. The reason the fix is depth-proof |
includes/password_gate.php |
Enforces the password change on every request, which is why there was no way around the broken page |
.htaccess, deploy/nginx/freeitsm.conf
|
The rewrites that create the second address for each of these pages |
includes/waffle-menu.php |
The working copy of the same call β _pathPrefix + 'api/myaccount/change_password.php'. This is why changing your password from the menu never broke, and why the fault could sit unnoticed on an established installation |
Each of the six call sites was rendered at both of its addresses and the emitted path compared, rather than reasoning about what it ought to produce. The two gated pages were rendered with a forged session so the password form and the one-time-code form actually appeared.
| Page requested | Emitted path |
|---|---|
/auth/forgot-password.php |
/freeitsm-app/api/auth/request_password_reset.php |
/forgot-password |
/freeitsm-app/api/auth/request_password_reset.php |
/auth/reset-password.php |
/freeitsm-app/api/auth/reset_password.php |
/reset-password |
/freeitsm-app/api/auth/reset_password.php |
/auth/force_password_change.php |
/freeitsm-app/api/myaccount/change_password.php |
/force_password_change.php |
/freeitsm-app/api/myaccount/change_password.php |
/auth/login.php |
/freeitsm-app/api/myaccount/verify_login_otp.php |
/login |
/freeitsm-app/api/myaccount/verify_login_otp.php |
The first run of that table is what caught the third fault: the two forgot/reset rows came back as /api/auth/β¦ with the subfolder missing, which is BASE_URL undefined and the '/' fallback firing. A rendered page that looks right is not a working one, and the two are only distinguishable by reading the actual bytes.
The endpoint was then actually called, at the address the page now emits, with a deliberately wrong current password:
HTTP 200
{"success":false,"error":"Current password is incorrect"}
A refusal is the proof wanted here: it means the request reached the endpoint and was processed, which a 404 never is.
The success path was read rather than assumed. api/myaccount/change_password.php clears $_SESSION['password_expired'] and sets must_change_password = 0 on the account. Had it not, fixing the fetch would have replaced a dead button with an endless loop back to the same page.
A sweep for the same shape elsewhere. Every page reachable at two depths β the CSAT survey, the asset QR scan page, the integrations provider and help pages, the OAuth callbacks β was checked for depth-relative fetch, action, href and src. The four in auth/ were the only instances.
- If you cannot get past the "Password Expired" screen on a new installation, this is it. Update, and the button works.
- If you have two-factor authentication switched on and sign-in stopped completing, that is the same bug on a different page, fixed in the same update.
- Nothing needs reconfiguring, and nothing was lost. No password was ever changed to something unexpected; the request simply never arrived.
- An established installation was never affected in the way the report describes, because changing your password from the user menu uses a different, correct copy of the same call. The fault only shows on the pages you meet before you are properly inside.
- Bugs resolved β the index of write-ups like this one
- Root Folder Tidy β the move that gave every one of these pages two addresses, and the background to issue #68, which this fix is the other half of
- Root Folder Tidy β why every one of these pages has two addresses
- Running on nginx β the rewrites that create the second address
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)