Compare commits

..
Author SHA1 Message Date
Graham Steffaniak f3974b28e5 fix onlyoffice csp security regression (#2876) 2026-08-30 11:30:12 -05:00
Ethan Lee Qi Yang ea85c50946 fix(contributing): typo from improtant to important (#2873) 2026-08-30 08:48:32 -05:00
be47c7a1e7 Beta/v2.0.3 (#2870)
Co-authored-by: Aditya Raj Singh <github@adityaraj.com>
Co-authored-by: tbsagdic <86909795+tbsagdic@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:53:51 -05:00
7 changed files with 102 additions and 197 deletions

No files matched your search

-41
View File
@@ -33,44 +33,3 @@ jobs:
context: .
file: ./_docker/Dockerfile.playwright-general
push: false
create_beta_release:
needs: [test_playwright]
if: startsWith(github.event.head_commit.message, 'Beta/v')
name: Create Beta Release
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
fetch-depth: 0
- name: Extract version from commit message
id: extract_version
shell: bash
run: |
tag_name=$(echo "${{ github.event.head_commit.message }}" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-beta
if [ -z "$tag_name" ] || [ "$tag_name" = "-beta" ]; then
echo "Could not extract version from: ${{ github.event.head_commit.message }}"
exit 1
fi
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
- name: Tag release commit on main
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
TAG="${{ steps.extract_version.outputs.tag_name }}"
git tag -f "$TAG" "${{ github.sha }}"
git push origin "refs/tags/$TAG" --force
- name: Create Release
uses: softprops/action-gh-release@v2
with:
target_commitish: ${{ github.sha }}
token: ${{ secrets.PAT }}
tag_name: ${{ steps.extract_version.outputs.tag_name }}
prerelease: false
make_latest: false
draft: false
generate_release_notes: true
overwrite_existing: true
name: ${{ steps.extract_version.outputs.tag_name }}
@@ -1,88 +0,0 @@
name: Promote Beta to Stable
on:
workflow_dispatch:
inputs:
version:
description: "Version to promote (format: 0.0.0)"
required: true
type: string
jobs:
promote-beta-to-stable:
name: Promote Beta to Stable
runs-on: ubuntu-24.04-arm
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }} # Uses Personal Access Token for branch operations
- name: Validate version format
id: validate_version
run: |
VERSION="${{ github.event.inputs.version }}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ ERROR: Version must be in '0.0.0' format."
exit 1
fi
echo "✅ Version format validated: $VERSION"
- name: Check if beta branch exists
id: check_beta_branch
run: |
VERSION="${{ github.event.inputs.version }}"
BETA_BRANCH="beta/v$VERSION"
if ! git ls-remote --exit-code origin "$BETA_BRANCH"; then
echo "❌ ERROR: Beta branch '$BETA_BRANCH' does not exist!"
exit 1
fi
echo "✅ Beta branch '$BETA_BRANCH' exists."
- name: Check if stable branch already exists
id: check_stable_branch
run: |
VERSION="${{ github.event.inputs.version }}"
STABLE_BRANCH="stable/v$VERSION"
if git ls-remote --exit-code origin "$STABLE_BRANCH"; then
echo "❌ ERROR: Stable branch '$STABLE_BRANCH' already exists!"
exit 1
fi
echo "✅ Stable branch '$STABLE_BRANCH' does not exist."
- name: Create and push new stable branch
run: |
VERSION="${{ github.event.inputs.version }}"
BETA_BRANCH="beta/v$VERSION"
STABLE_BRANCH="stable/v$VERSION"
# Fetch latest changes
git fetch origin "$BETA_BRANCH"
# Create new stable branch from beta branch
git checkout -b "$STABLE_BRANCH" origin/"$BETA_BRANCH"
# Push new stable branch to remote
git push origin "$STABLE_BRANCH"
echo "✅ Successfully created stable branch '$STABLE_BRANCH' from '$BETA_BRANCH'."
- name: Merge beta into stable
run: |
VERSION="${{ github.event.inputs.version }}"
BETA_BRANCH="beta/v$VERSION"
STABLE_BRANCH="stable/v$VERSION"
# Checkout stable branch
git checkout "$STABLE_BRANCH"
# Merge beta into stable
git merge --no-ff "origin/$BETA_BRANCH" -m "Merge $BETA_BRANCH into $STABLE_BRANCH"
# Push merge changes
git push origin "$STABLE_BRANCH"
echo "✅ Successfully merged '$BETA_BRANCH' into '$STABLE_BRANCH'."
-66
View File
@@ -1,66 +0,0 @@
name: stable release
on:
push:
branches:
- "stable/v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
test_playwright:
name: Test Playwright - regular
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- uses: actions/setup-node@v4
- working-directory: frontend
run: npm i && npm run build
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- working-directory: backend
run: go build -o filebrowser .
- name: Build
uses: docker/build-push-action@v6
with:
context: .
file: ./_docker/Dockerfile.playwright-general
push: false
create_release_tag:
needs: [ test_playwright ]
name: Create Release
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Extract branch name
shell: bash
run: |
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
tag_name=$(echo "$original_branch" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-stable
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
id: extract_branch
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Create Release
uses: softprops/action-gh-release@v2
with:
target_commitish: ${{ github.sha }}
token: ${{ secrets.PAT }}
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
prerelease: false
make_latest: true
draft: false
generate_release_notes: true
name: ${{ steps.extract_branch.outputs.tag_name }}
+5
View File
@@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v2.0.4
**Bugfixes**:
- OnlyOffice editor failed to load after the CSP security fix; allow configured `integrations.office.url` and `integrations.office.internalUrl` origins in `script-src` ([#2874](https://github.com/gtsteffaniak/filebrowser/issues/2874)).
## v2.0.3
**Security**:
+1 -1
View File
@@ -7,7 +7,7 @@ Thank you for your interest in contributing to FileBrowser Quantum! This guide w
- **Go 1.25+** (see `backend/go.mod`)
- **Node.js 22.0.0+** with npm 9.0.0+ (see `frontend/package.json`)
- **Docker** (optional, for containerized development)
- **Git** especially improtant on windows -- needed for bash support.
- **Git** especially important on Windows -- needed for bash support.
### Optional Tools
@@ -50,6 +50,10 @@ func TestSessionCookie_NotSecureOnHTTP(t *testing.T) {
}
func TestSpaContentSecurityPolicy_BlocksUntrustedScripts(t *testing.T) {
prev := settings.Config.Integrations.OnlyOffice
settings.Config.Integrations.OnlyOffice = settings.OnlyOffice{}
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = prev })
csp := spaContentSecurityPolicy("testnonce")
if csp != "script-src 'self' 'nonce-testnonce' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com" {
t.Fatalf("unexpected CSP: %s", csp)
@@ -61,3 +65,58 @@ func TestSpaContentSecurityPolicy_BlocksUntrustedScripts(t *testing.T) {
t.Fatal("CSP should only restrict script-src to avoid breaking frames, blobs, and external assets")
}
}
func TestSpaContentSecurityPolicy_IncludesOnlyOfficeOrigins(t *testing.T) {
prev := settings.Config.Integrations.OnlyOffice
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = prev })
tests := []struct {
name string
oo settings.OnlyOffice
want string
}{
{
name: "url only",
oo: settings.OnlyOffice{Url: "http://localhost:9052"},
want: "script-src 'self' 'nonce-testnonce' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com http://localhost:9052",
},
{
name: "internal url only",
oo: settings.OnlyOffice{InternalUrl: "http://onlyoffice-internal:80"},
want: "script-src 'self' 'nonce-testnonce' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com http://onlyoffice-internal:80",
},
{
name: "both urls",
oo: settings.OnlyOffice{
Url: "https://office.example.com",
InternalUrl: "http://onlyoffice-internal",
},
want: "script-src 'self' 'nonce-testnonce' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com https://office.example.com http://onlyoffice-internal",
},
{
name: "duplicate origins",
oo: settings.OnlyOffice{
Url: "http://localhost:9052",
InternalUrl: "http://localhost:9052",
},
want: "script-src 'self' 'nonce-testnonce' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com http://localhost:9052",
},
{
name: "invalid urls ignored",
oo: settings.OnlyOffice{
Url: "not-a-url",
InternalUrl: "://bad",
},
want: "script-src 'self' 'nonce-testnonce' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
settings.Config.Integrations.OnlyOffice = tt.oo
if got := spaContentSecurityPolicy("testnonce"); got != tt.want {
t.Fatalf("unexpected CSP:\ngot: %s\nwant: %s", got, tt.want)
}
})
}
}
+37 -1
View File
@@ -61,10 +61,46 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
// spaContentSecurityPolicy restricts only scripts. srcdoc preview frames inherit
// this header, blocking inline scripts without limiting frames, images, or API calls.
func spaContentSecurityPolicy(nonce string) string {
return fmt.Sprintf(
policy := fmt.Sprintf(
"script-src 'self' 'nonce-%s' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com",
nonce,
)
for _, origin := range onlyOfficeScriptSrcOrigins() {
policy += " " + origin
}
return policy
}
func onlyOfficeScriptSrcOrigins() []string {
oo := settings.Config.Integrations.OnlyOffice
candidates := []string{
cspOriginFromURL(oo.Url),
cspOriginFromURL(oo.InternalUrl),
}
var origins []string
seen := make(map[string]struct{}, len(candidates))
for _, origin := range candidates {
if origin == "" {
continue
}
if _, ok := seen[origin]; ok {
continue
}
seen[origin] = struct{}{}
origins = append(origins, origin)
}
return origins
}
func cspOriginFromURL(raw string) string {
if raw == "" {
return ""
}
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return ""
}
return u.Scheme + "://" + u.Host
}
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestContext, file, contentType string) (int, error) {