Compare commits

..

5 Commits

Author SHA1 Message Date
Fred KISSIE 56fcb80b23 💄 fix labels dropdown flicker if no changes applied 2026-07-20 17:36:45 +01:00
Owen 9561d23f1e Resovle endcoding issue 2026-07-19 14:41:07 -04:00
Owen a2e1c7b751 Go to domains if no domains 2026-07-18 16:30:29 -04:00
Owen 9e2ec72ced Fix missing resource id in cache 2026-07-18 16:15:30 -04:00
Owen 02fe1f3abd Claify that this is only for the cloud 2026-07-18 11:53:56 -04:00
13 changed files with 122 additions and 27 deletions
+5 -5
View File
@@ -77,7 +77,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -149,7 +149,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -204,7 +204,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -264,7 +264,7 @@ jobs:
shell: bash shell: bash
- name: Install Go - name: Install Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: 1.25 go-version: 1.25
@@ -407,7 +407,7 @@ jobs:
shell: bash shell: bash
- name: Login to GitHub Container Registry (for cosign) - name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '24' node-version: '24'
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with: with:
days-before-stale: 14 days-before-stale: 14
days-before-close: 14 days-before-close: 14
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Node - name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '24' node-version: '24'
+1 -1
View File
@@ -41,7 +41,7 @@ services:
- 80:80 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode
traefik: traefik:
image: traefik:v3.7 image: traefik:v3.6
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service network_mode: service:gerbil # Ports appear on the gerbil service
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- 80:80{{end}} - 80:80{{end}}
traefik: traefik:
image: docker.io/traefik:v3.7 image: docker.io/traefik:v3.6
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}} {{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
+3
View File
@@ -1995,6 +1995,9 @@
"domainPickerSubdomain": "Subdomain: {subdomain}", "domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}", "domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Show More", "domainPickerShowMore": "Show More",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Select Region", "regionSelectorTitle": "Select Region",
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.", "domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.", "regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
+13 -11
View File
@@ -30,14 +30,14 @@ export const NotifyTrialExpiring = ({
const isLastDay = daysRemaining === 1; const isLastDay = daysRemaining === 1;
const previewText = hasEnded const previewText = hasEnded
? `Your trial for ${orgName} has ended.` ? `Your cloud trial for ${orgName} has ended.`
: isLastDay : isLastDay
? `Your trial for ${orgName} ends tomorrow.` ? `Your cloud trial for ${orgName} ends tomorrow.`
: `Your trial for ${orgName} ends in ${daysRemaining} days.`; : `Your cloud trial for ${orgName} ends in ${daysRemaining} days.`;
const heading = hasEnded const heading = hasEnded
? "Your Trial Ended" ? "Your Cloud Trial Ended"
: "Your Trial is Ending Soon"; : "Your Cloud Trial is Ending Soon";
return ( return (
<Html> <Html>
@@ -55,7 +55,7 @@ export const NotifyTrialExpiring = ({
{hasEnded ? ( {hasEnded ? (
<> <>
<EmailText> <EmailText>
Your free trial for{" "} Your cloud free trial for{" "}
<strong>{orgName}</strong> ended on{" "} <strong>{orgName}</strong> ended on{" "}
<strong>{trialEndsAt}</strong>. Your account <strong>{trialEndsAt}</strong>. Your account
has been moved to the free plan, which has been moved to the free plan, which
@@ -64,10 +64,11 @@ export const NotifyTrialExpiring = ({
<EmailText> <EmailText>
Some features and resources may now be Some features and resources may now be
restricted. To restore full restricted. To restore full access and
access and continue using all the features continue using all the features you had
you had during your trial, please upgrade to during your trial, please upgrade to a paid
a paid plan. plan. This does not effect any self hosted
licenses.
</EmailText> </EmailText>
<EmailText> <EmailText>
@@ -93,7 +94,8 @@ export const NotifyTrialExpiring = ({
<EmailText> <EmailText>
After your trial ends, your account will be After your trial ends, your account will be
moved to the free plan and some moved to the free plan and some
functionality may be restricted. functionality may be restricted. This does
not effect any self hosted licenses.
</EmailText> </EmailText>
<EmailText> <EmailText>
+34 -3
View File
@@ -15,10 +15,41 @@ function getSegmentRegex(patternPart: string): RegExp {
return regex; return regex;
} }
// Decodes percent-encoding (so an encoded slash like `%2F` is treated as a
// real path separator, matching what most backends will do) and then
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment
// or a wildcard-swallowed sequence under `/public/*`.
function decodeAndResolvePath(p: string): string[] {
const rawParts = p.split("/").filter(Boolean);
const resolved: string[] = [];
for (const rawPart of rawParts) {
let part: string;
try {
part = decodeURIComponent(rawPart);
} catch {
part = rawPart;
}
// an encoded slash can turn one raw segment into several real ones
for (const segment of part.split("/").filter(Boolean)) {
if (segment === ".") {
continue;
} else if (segment === "..") {
resolved.pop();
} else {
resolved.push(segment);
}
}
}
return resolved;
}
export function isPathAllowed(pattern: string, path: string): boolean { export function isPathAllowed(pattern: string, path: string): boolean {
const normalize = (p: string) => p.split("/").filter(Boolean); const patternParts = pattern.split("/").filter(Boolean);
const patternParts = normalize(pattern); const pathParts = decodeAndResolvePath(path);
const pathParts = normalize(path);
function matchSegments( function matchSegments(
patternIndex: number, patternIndex: number,
@@ -236,6 +236,38 @@ function runTests() {
"Root path should not match non-root path" "Root path should not match non-root path"
); );
// Path traversal / encoded-slash bypass regression tests
assertEquals(
isPathAllowed("public/*", "public/../admin"),
false,
"Literal .. traversal out of an allowed prefix must not match"
);
assertEquals(
isPathAllowed("public/*", "public%2F..%2Fadmin"),
false,
"Encoded-slash traversal out of an allowed prefix must not match"
);
assertEquals(
isPathAllowed("public/*", "public%2f..%2fadmin%2ffile"),
false,
"Encoded-slash traversal is case-insensitively decoded before matching"
);
assertEquals(
isPathAllowed("admin/*", "public/../admin/secret"),
true,
".. traversal INTO a restricted path should still be caught by its own rule"
);
assertEquals(
isPathAllowed("public/*", "public%2Ffoo"),
true,
"Encoded slash without traversal should still resolve and match normally"
);
assertEquals(
isPathAllowed("public/*", "public/./foo"),
true,
"Single-dot segments are a no-op and should not affect matching"
);
console.log("All path matching tests passed!"); console.log("All path matching tests passed!");
} }
+1 -1
View File
@@ -418,7 +418,7 @@ export async function verifyResourceSession(
} }
// check for HTTP Basic Auth header // check for HTTP Basic Auth header
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`; const clientHeaderAuthKey = `headerAuth:${resource.resourceId}:${clientHeaderAuth}`;
if (headerAuth && clientHeaderAuth) { if (headerAuth && clientHeaderAuth) {
if (localCache.get(clientHeaderAuthKey)) { if (localCache.get(clientHeaderAuthKey)) {
logger.debug( logger.debug(
+25 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { import {
@@ -43,10 +43,12 @@ import {
CheckCircle2, CheckCircle2,
ChevronsUpDown, ChevronsUpDown,
ExternalLink, ExternalLink,
Globe,
KeyRound, KeyRound,
Zap Zap
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@/hooks/usePaidStatus"; import { usePaidStatus } from "@/hooks/usePaidStatus";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -494,6 +496,28 @@ export default function DomainPicker({
const hasMoreProvided = const hasMoreProvided =
sortedAvailableOptions.length > providedDomainsShown; sortedAvailableOptions.length > providedDomainsShown;
const noDomainsAvailable =
!loadingDomains &&
organizationDomains.length === 0 &&
(build === "oss" || hideFreeDomain || requiresPaywall);
if (noDomainsAvailable) {
return (
<Alert>
<Globe className="h-4 w-4" />
<AlertTitle>{t("domainPickerNoDomainsAvailableTitle")}</AlertTitle>
<AlertDescription className="space-y-3">
<p>{t("domainPickerNoDomainsAvailableDescription")}</p>
<Button asChild size="sm" variant="outline">
<Link href={`/${orgId}/settings/domains`}>
{t("domainPickerNoDomainsAvailableAction")}
</Link>
</Button>
</AlertDescription>
</Alert>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
+3
View File
@@ -95,7 +95,10 @@ export function useOptimisticLabels({
} }
async function refresh() { async function refresh() {
// Only refresh if the labels have been modified
if (pendingActions.length > 0) {
router.refresh(); router.refresh();
}
setPendingActions([]); setPendingActions([]);
} }