Skip to content

Commit cc11189

Browse files
authored
release(v3.23.0): authentication and authorization hardening
- security(auth): harden TOTP verification and account settings - security(oidc): bind accounts by immutable provider identity - security(files): enforce upload, callback, and path boundaries
1 parent df39332 commit cc11189

36 files changed

Lines changed: 4127 additions & 350 deletions

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
# Changelog
22

3+
## Changes 07/25/2026 (v3.23.0)
4+
5+
`release(v3.23.0): authentication and authorization hardening`
6+
7+
**Commit message**
8+
9+
```text
10+
release(v3.23.0): authentication and authorization hardening
11+
12+
- security(auth): harden TOTP verification and account settings
13+
- security(oidc): bind accounts by immutable provider identity
14+
- security(files): enforce upload, callback, and path boundaries
15+
```
16+
17+
**Fixed**
18+
19+
- TOTP verification now enforces persistent account-wide and source-wide attempt budgets across all pending-login verification paths, and the alternate auth handler now resolves the shared TOTP verifier correctly, so changing sessions or client addresses cannot reset the second-factor limit.
20+
- Successfully accepted TOTP time steps are now claimed atomically per account and authenticator secret, preventing the same still-valid code from completing authentication more than once.
21+
- Successful TOTP verification clears the account budget without counting ordinary successful logins against shared-network failure limits.
22+
- TOTP enrollment now remains pending until a valid code confirms the new authenticator, and enrollment, disablement, and recovery-code rotation require a recent full authentication.
23+
- OIDC logins now bind the validated issuer-and-subject identity to a local account, ignore unverified email for naming, and cannot attach a mutable username claim to an existing local account.
24+
- Upload and rename validation now rejects trailing-dot filenames across regular, resumable, shared, and WebDAV write paths so filesystem or web-server normalization cannot bypass blocked executable names.
25+
- Logical folder paths now reject raw or encoded `.` and `..` segments before ACL evaluation or storage access; local writes use boundary-aware containment before directory creation, and WebDAV operations cannot escape the configured source root.
26+
- ZIP, 7z, and RAR extraction now occurs in a private temporary workspace on the same local storage volume, and validated regular files are placed atomically only through real in-root destination directories; pre-existing destination symlinks can no longer redirect extraction or cleanup outside the storage root.
27+
- ONLYOFFICE save callbacks now require a valid HS256 JWT from the configured Document Server; omitting the callback JWT no longer falls back to trusting the unsigned request body.
28+
- Cross-parent folder renames now enforce destination move rights and matching ownership boundaries, preventing a folder owner from moving their tree into another user's folder or the admin-controlled root.
29+
30+
**Upgrade notes**
31+
32+
- **Breaking security change for existing OIDC deployments:** OIDC identities created before v3.23.0 do not have an issuer/subject binding and cannot continue matching an existing local account automatically.
33+
- **ONLYOFFICE callback security change:** Before upgrading, ensure the Document Server has outgoing/outbox JWTs enabled and uses the same secret configured in FileRise. Correctly configured deployments require no data migration.
34+
- Older or misconfigured Document Servers that omit callback JWTs will be unable to save until their outgoing JWT configuration is corrected. `ONLYOFFICE_ALLOW_UNSIGNED_CALLBACKS=1` is available only as a temporary, explicitly unsafe recovery measure; remove it immediately after confirming a signed document save.
35+
- After five failed TOTP submissions, that account waits up to 15 minutes before another verification attempt.
36+
- Sessions whose full authentication is more than five minutes old must sign in again before changing TOTP security settings; remember-me restoration alone does not satisfy this check.
37+
- Existing non-OIDC users, TOTP secrets, sessions, Docker volumes, and deployment configuration require no migration.
38+
- Before upgrading an OIDC deployment, verify a working local administrator login and back up the `users` directory.
39+
- On the first OIDC login for an existing account, FileRise presents a one-time guided link prompt for that account's local password and, when configured, TOTP. This confirmation works even when ordinary form login is disabled.
40+
- New OIDC identities whose proposed username does not collide with an existing local account continue to auto-provision normally when OIDC auto-provisioning is enabled; they are bound to the validated issuer and subject immediately and do not receive the guided link prompt.
41+
- OIDC-only users without a known local password require an administrator-assisted password reset or account migration. Complete this preparation before upgrading any deployment whose only administrator uses OIDC.
42+
- See `docs/wiki/oidc sso.md` under **Upgrading existing OIDC accounts to v3.23.0** for the complete migration procedure.
43+
44+
---
45+
346
## Changes 07/13/2026 (v3.22.0)
447

548
`release(v3.22.0): generate unique keys for pristine manual installs`

config/config.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
define('TIMEZONE', 'America/New_York');
4141
define('DATE_TIME_FORMAT','m/d/y h:iA');
4242
define('TOTAL_UPLOAD_SIZE', '5G');
43-
define('REGEX_FOLDER_NAME','/^(?!^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$)(?!.*[. ]$)(?:[^<>:"\/\\\\|?*\x00-\x1F]{1,255})(?:[\/\\\\][^<>:"\/\\\\|?*\x00-\x1F]{1,255})*$/xu');
43+
define('REGEX_FOLDER_NAME','/^(?!^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$)(?!\.{1,2}(?:[\/\\\\]|$))(?!.*[\/\\\\]\.{1,2}(?:[\/\\\\]|$))(?!.*[. ]$)(?:[^<>:"\/\\\\|?*\x00-\x1F]{1,255})(?:[\/\\\\][^<>:"\/\\\\|?*\x00-\x1F]{1,255})*$/xu');
4444
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
4545
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
4646
define('REGEX_USER', '/^(?!\.{1,2}$)[\p{L}\p{N}_\- .@]+$/u');
@@ -62,6 +62,8 @@
6262
define('ONLYOFFICE_JWT_SECRET', 'test123456');
6363
define('ONLYOFFICE_DOCS_ORIGIN', 'http://192.168.1.61'); // your Document Server
6464
define('ONLYOFFICE_DEBUG', true);
65+
// Emergency migration only; leaves callback bodies unauthenticated.
66+
// define('ONLYOFFICE_ALLOW_UNSIGNED_CALLBACKS', false);
6567
*/
6668
if (!defined('OFFICE_SNIPPET_MAX_BYTES')) {
6769
define('OFFICE_SNIPPET_MAX_BYTES', 5 * 1024 * 1024); // 5 MiB
@@ -456,6 +458,7 @@ function fr_forget_authenticated_user(bool $clearRememberCookie = false): void
456458
$_SESSION['folderOnly'],
457459
$_SESSION['readOnly'],
458460
$_SESSION['disableUpload'],
461+
$_SESSION['authenticated_at'],
459462
$_SESSION['pending_login_user'],
460463
$_SESSION['pending_login_secret'],
461464
$_SESSION['pending_login_remember_me']
@@ -528,6 +531,7 @@ function fr_forget_authenticated_user(bool $clearRememberCookie = false): void
528531
}
529532

530533
$_SESSION["authenticated"] = true;
534+
unset($_SESSION['authenticated_at']);
531535
$_SESSION["username"] = $payload["username"];
532536
$perms = loadUserPermissions($payload["username"]);
533537
$_SESSION["folderOnly"] = $perms['folderOnly'] ?? false;
@@ -591,6 +595,7 @@ function fr_forget_authenticated_user(bool $clearRememberCookie = false): void
591595

592596
$username = $_SERVER[$hdrKey];
593597
$_SESSION['authenticated'] = true;
598+
$_SESSION['authenticated_at'] = time();
594599
$_SESSION['username'] = $username;
595600

596601
// ◾ lookup actual role instead of forcing admin

docs/wiki/oidc sso.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,69 @@ Advanced override (in `config/config.php` or env):
3737

3838
- `FR_OIDC_AUTO_CREATE` (`true`/`false`)
3939

40+
FileRise binds each OIDC account using the provider's immutable issuer and subject
41+
identifiers. `preferred_username` is used only to name a new local account.
42+
Verified email can be used when no valid preferred username is available.
43+
Neither claim can attach an unbound OIDC identity to an existing local account.
44+
45+
Bindings are stored in `users/oidc_identities.json`; include this file in backups
46+
with the rest of the users directory.
47+
48+
### Upgrading existing OIDC accounts to v3.23.0
49+
50+
> **Breaking security change:** OIDC accounts used before v3.23.0 do not yet
51+
> have an issuer/subject binding. Their first OIDC login after upgrading cannot
52+
> automatically match the existing local account.
53+
54+
This affects only upgrades of deployments with existing OIDC users. It does not
55+
change local-password accounts, stored files, permissions, TOTP secrets, Docker
56+
volumes, or the OIDC provider configuration.
57+
58+
#### Before upgrading
59+
60+
1. Back up the complete FileRise `users` directory.
61+
2. Open a private browser window and confirm that at least one administrator can
62+
sign in with a local username and password.
63+
3. Identify accounts that use OIDC exclusively and do not have a known local
64+
password.
65+
4. While an administrator is still signed in, assign those users temporary
66+
local passwords or plan an administrator-assisted account migration.
67+
5. If the only administrator uses OIDC, set and test a local administrator
68+
password before upgrading. Do not rely on an existing browser session as the
69+
only recovery method.
70+
71+
#### Link each existing account after upgrading
72+
73+
1. Start the normal FileRise OIDC login.
74+
2. Authenticate as the corresponding user at the configured identity provider.
75+
3. When FileRise detects the existing unbound local account, it displays a
76+
one-time account-link confirmation with the local username fixed and
77+
read-only.
78+
4. Enter that account's local password. If the account has TOTP enabled,
79+
complete the existing TOTP prompt as well.
80+
5. FileRise records the immutable issuer/subject binding in
81+
`users/oidc_identities.json`.
82+
6. Sign out and verify that a new OIDC login returns to the same FileRise
83+
account with its existing role and permissions.
84+
85+
The guided confirmation is available only after a validated OIDC callback,
86+
expires after five minutes, and works even when ordinary form login is disabled.
87+
Canceling it clears the pending session without modifying the local account.
88+
89+
Repeat the procedure for each existing OIDC account and include
90+
`users/oidc_identities.json` in future backups. Retire any temporary passwords
91+
according to your local credential policy after verifying the bindings.
92+
93+
If a user attempts OIDC login before completing the link, FileRise rejects a
94+
claim that collides with the existing local username. The rejection does not
95+
delete or modify the existing account.
96+
97+
OIDC-only users without a known local password require an administrator-assisted
98+
password reset or migration to a newly auto-provisioned OIDC account. FileRise
99+
deliberately does not perform automatic first-login matching because a mutable
100+
claim cannot prove ownership of an existing account and would recreate the
101+
account-takeover vulnerability.
102+
40103
---
41104

42105
## Admin group mapping

docs/wiki/onlyoffice.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,71 @@ Supported:
1616
- Document Server origin (e.g. `https://docs.example.com`)
1717
- JWT secret (shared with your Document Server)
1818
- Optional: Public origin (if callbacks must use a different public URL)
19-
3. Use **Run tests** in the admin panel to validate connectivity.
19+
3. Ensure outgoing/outbox JWTs are enabled on the Document Server.
20+
4. Use **Run tests** in the admin panel to validate connectivity and confirm callback JWT enforcement is active.
21+
5. Open, edit, and save a test document to verify the Document Server can complete a signed callback.
22+
23+
For a Docker Document Server, configure a stable secret when creating the container:
24+
25+
```yaml
26+
environment:
27+
JWT_ENABLED: "true"
28+
JWT_SECRET: "replace-with-the-same-secret-used-by-filerise"
29+
```
30+
31+
For a package installation or an older Document Server, ensure all three values are configured in `/etc/onlyoffice/documentserver/local.json` and restart the ONLYOFFICE services:
32+
33+
```json
34+
{
35+
"services": {
36+
"CoAuthoring": {
37+
"token": {
38+
"enable": {
39+
"request": {
40+
"outbox": true
41+
}
42+
}
43+
},
44+
"secret": {
45+
"outbox": {
46+
"string": "replace-with-the-same-secret-used-by-filerise"
47+
}
48+
}
49+
}
50+
}
51+
}
52+
```
53+
54+
Do not edit `default.json`; ONLYOFFICE can replace that file during upgrades.
55+
56+
---
57+
58+
## Upgrading to FileRise v3.23.0
59+
60+
FileRise v3.23.0 rejects missing and invalid callback JWTs. Previous FileRise versions accepted an unsigned callback body when the separate FileRise callback URL token was valid.
61+
62+
Before upgrading:
63+
64+
1. Confirm the JWT secret configured in FileRise matches the Document Server's `JWT_SECRET` or outbox secret.
65+
2. Confirm outgoing/outbox request tokens are enabled.
66+
3. Restart the Document Server after changing its JWT configuration.
67+
4. After upgrading FileRise, use **Run tests**, then edit and save a test document.
68+
69+
Existing documents, accounts, and FileRise storage require no migration. A correctly configured Document Server continues working normally.
70+
71+
If saves fail because an older deployment is not sending callback JWTs, correct the Document Server configuration first. As a temporary recovery measure only, set this environment variable on the FileRise container or PHP service:
72+
73+
```text
74+
ONLYOFFICE_ALLOW_UNSIGNED_CALLBACKS=1
75+
```
76+
77+
For a manual FileRise installation, the equivalent temporary override is:
78+
79+
```php
80+
define('ONLYOFFICE_ALLOW_UNSIGNED_CALLBACKS', true);
81+
```
82+
83+
This override restores the old unsigned-body behavior and weakens callback authentication. Keep it enabled only long enough to correct the Document Server, restart it, and confirm a successful signed save. Then remove the override and restart FileRise. The Admin Panel displays a security warning while the override is active.
2084

2185
---
2286

@@ -63,3 +127,5 @@ You can lock ONLYOFFICE settings in `config/config.php` by defining:
63127
- `ONLYOFFICE_PUBLIC_ORIGIN`
64128
65129
When any of these are defined, the admin UI reflects the locked values.
130+
131+
`ONLYOFFICE_ALLOW_UNSIGNED_CALLBACKS` is an emergency compatibility override, not a normal integration setting. It defaults to `false` and should not remain enabled.

public/api/folder/renameFolder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* @OA\Post(
66
* path="/api/folder/renameFolder.php",
77
* summary="Rename or move a folder",
8-
* description="Requires authentication, CSRF token, scope checks on old and new paths, and (for non-admins) ownership of the source folder.",
8+
* description="Requires authentication, CSRF, source ownership, and destination move authorization when the parent changes.",
99
* operationId="renameFolder",
1010
* tags={"Folders"},
1111
* security={{"cookieAuth": {}}},
@@ -29,4 +29,4 @@
2929
require_once __DIR__ . '/../../../config/config.php';
3030

3131
$folderController = new \FileRise\Http\Controllers\FolderController();
32-
$folderController->renameFolder();
32+
$folderController->renameFolder();

public/js/adminOnlyOffice.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ function attachOnlyOfficeTests(container) {
212212
</div>
213213
<ul id="ooTestResults" class="list-unstyled" style="margin:0;"></ul>
214214
<small class="text-muted">
215-
These tests check FileRise config, callback reachability, CSP/script loading, and iframe embedding.
215+
These tests check FileRise config, callback enforcement, reachability, CSP/script loading,
216+
and iframe embedding. Complete one real document save after an upgrade to confirm the
217+
Document Server sends a valid outgoing callback JWT.
216218
</small>
217219
</div>
218220
`;
@@ -257,8 +259,18 @@ function attachOnlyOfficeTests(container) {
257259
} else {
258260
out.appendChild(ooRow('FileRise status', 'fail', `HTTP ${r.status}`));
259261
}
262+
out.appendChild(
263+
ooRow(
264+
'Callback JWT enforcement',
265+
statusJson.callbackJwtRequired ? 'ok' : 'warn',
266+
statusJson.callbackJwtRequired
267+
? 'Required — unsigned callbacks are rejected'
268+
: 'UNSAFE compatibility override enabled'
269+
)
270+
);
260271
} catch (e) {
261272
out.appendChild(ooRow('FileRise status', 'fail', (e && e.message) || 'Network error'));
273+
out.appendChild(ooRow('Callback JWT enforcement', 'warn', 'Could not verify'));
262274
}
263275

264276
// 2) Secret presence (fresh read)
@@ -487,6 +499,15 @@ export function initOnlyOfficeUI({ config }) {
487499
// Initial values
488500
const enabled = !!onlyCfg.enabled;
489501
const docsOrigin = onlyCfg.docsOrigin || '';
502+
const unsignedCallbacksAllowed = !!onlyCfg.unsignedCallbacksAllowed;
503+
504+
const jwtState = document.createElement('div');
505+
jwtState.className = unsignedCallbacksAllowed ? 'alert alert-danger' : 'alert alert-success';
506+
jwtState.style.marginTop = '12px';
507+
jwtState.textContent = unsignedCallbacksAllowed
508+
? 'Security warning: unsigned ONLYOFFICE callbacks are temporarily allowed. Configure outgoing JWTs on the Document Server and remove ONLYOFFICE_ALLOW_UNSIGNED_CALLBACKS.'
509+
: 'Callback JWT enforcement is enabled. Unsigned or invalid save callbacks are rejected.';
510+
sec.appendChild(jwtState);
490511

491512
const enabledEl = document.getElementById('ooEnabled');
492513
const originEl = document.getElementById('ooDocsOrigin');

public/js/authModals.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,22 @@ function loadTOTPQRCode() {
262262
credentials: "include",
263263
headers: { "X-CSRF-Token": window.csrfToken }
264264
})
265-
.then(res => {
266-
if (!res.ok) throw new Error("Failed to fetch QR code: " + res.status);
265+
.then(async res => {
266+
if (!res.ok) {
267+
const contentType = res.headers.get("content-type") || "";
268+
const payload = contentType.includes("application/json")
269+
? await res.json().catch(() => ({}))
270+
: {};
271+
if (payload.reauth_required) {
272+
window.location.href = withBase("/index.html");
273+
return null;
274+
}
275+
throw new Error(payload.error || "Failed to fetch QR code: " + res.status);
276+
}
267277
return res.blob();
268278
})
269279
.then(blob => {
280+
if (!blob) return;
270281
const url = URL.createObjectURL(blob);
271282
document.getElementById("totpQRCodeImage").src = url;
272283
})

public/js/i18n.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ const translations = {
155155
"login": "Login",
156156
"remember_me": "Remember me",
157157
"login_oidc": "Login with OIDC",
158+
"oidc_link_confirm_button": "Confirm and link OIDC",
159+
"oidc_link_cancel_button": "Cancel account linking",
160+
"oidc_link_confirm_prompt": "Confirm the local password for “{username}” to securely link this OIDC identity.",
158161
"basic_http_login": "Use Basic HTTP Login",
159162

160163
// Change Password keys:

0 commit comments

Comments
 (0)