Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions agent/internal/agent/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,14 @@ func extractTarGz(archivePath, destPath string) error {
return fmt.Errorf("invalid tar entry: %s", header.Name)
}

parentDir := filepath.Dir(targetPath)
if resolvedParent, err := filepath.EvalSymlinks(parentDir); err == nil {
if !strings.HasPrefix(resolvedParent, filepath.Clean(destPath)+string(os.PathSeparator)) &&
resolvedParent != filepath.Clean(destPath) {
return fmt.Errorf("invalid tar entry (symlink traversal): %s", header.Name)
}
}

switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
Expand All @@ -976,6 +984,15 @@ func extractTarGz(archivePath, destPath string) error {
}
outFile.Close()
case tar.TypeSymlink:
linkTarget := header.Linkname
if !filepath.IsAbs(linkTarget) {
linkTarget = filepath.Join(filepath.Dir(targetPath), linkTarget)
}
resolvedLink := filepath.Clean(linkTarget)
if !strings.HasPrefix(resolvedLink, filepath.Clean(destPath)+string(os.PathSeparator)) &&
resolvedLink != filepath.Clean(destPath) {
return fmt.Errorf("invalid symlink target: %s -> %s", header.Name, header.Linkname)
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return fmt.Errorf("failed to create parent directory: %w", err)
}
Expand Down
15 changes: 12 additions & 3 deletions web/actions/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ import { startMigration } from "./migrations";
import { inngest } from "@/lib/inngest/client";

function isValidImageReferencePart(reference: string): boolean {
// Allow only characters that are valid in Docker tags/digests and avoid path traversal.
// Tags: letters, digits, underscores, periods and dashes; Digests: "algorithm:hex".
// This regex is intentionally conservative.
const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
const digestPattern = /^[A-Za-z0-9_+.-]+:[0-9a-fA-F]{32,256}$/;

Expand All @@ -54,6 +51,11 @@ function isValidImageReferencePart(reference: string): boolean {
);
}

function isValidImageNamePart(part: string): boolean {
const segmentPattern = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;
return part.split("/").every((segment) => segmentPattern.test(segment));
}

function parseImageReference(image: string): {
registry: string;
namespace: string;
Expand Down Expand Up @@ -117,6 +119,13 @@ export async function validateDockerImage(
};
}

if (!isValidImageNamePart(namespace) || !isValidImageNamePart(repository)) {
return {
valid: false,
error: "Invalid image name",
};
}

if (registry === "docker.io") {
const repoPath =
namespace === "library" ? repository : `${namespace}/${repository}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ services:
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{errors.map((err, i) => (
<div key={i}>{err.message}</div>
{errors.map((err) => (
<div key={err.message}>{err.message}</div>
))}
</AlertDescription>
</Alert>
Expand Down Expand Up @@ -335,8 +335,8 @@ services:
</AlertTitle>
<AlertDescription className="text-yellow-700/80 dark:text-yellow-500/80">
<ul className="list-disc list-inside space-y-1 mt-1">
{warnings.map((w, i) => (
<li key={i}>
{warnings.map((w) => (
<li key={`${w.service}-${w.message}`}>
{w.service ? <strong>{w.service}:</strong> : null}{" "}
{w.message}
</li>
Expand All @@ -352,8 +352,8 @@ services:
<AlertTitle>Errors ({errors.length})</AlertTitle>
<AlertDescription>
<ul className="list-disc list-inside space-y-1 mt-1">
{errors.map((e, i) => (
<li key={i}>
{errors.map((e) => (
<li key={`${e.service}-${e.message}`}>
{e.service ? <strong>{e.service}:</strong> : null}{" "}
{e.message}
</li>
Expand Down Expand Up @@ -482,8 +482,8 @@ services:
</AlertTitle>
<AlertDescription className="text-yellow-700/80 dark:text-yellow-500/80">
<ul className="list-disc list-inside space-y-1 mt-1">
{importResult.warnings.map((w, i) => (
<li key={i}>
{importResult.warnings.map((w) => (
<li key={`${w.service}-${w.message}`}>
{w.service ? <strong>{w.service}:</strong> : null}{" "}
{w.message}
</li>
Expand Down
2 changes: 1 addition & 1 deletion web/app/api/github/manifest/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get("code");

if (!code) {
if (!code || !/^[a-zA-Z0-9]+$/.test(code)) {
return NextResponse.redirect(
new URL("/dashboard/settings?github_error=missing_code", request.url),
);
Expand Down
69 changes: 0 additions & 69 deletions web/components/core/action-button.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion web/components/core/editable-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function EditableText({
}) {
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [inputValue, setInputValue] = useState(value);
const [inputValue, setInputValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

const handleFocus = () => {
Expand Down
29 changes: 16 additions & 13 deletions web/components/logs/log-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
parseAsStringLiteral,
useQueryState,
} from "nuqs";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import useSWR from "swr";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -116,7 +116,7 @@ function highlightMatches(text: string, search: string): React.ReactNode {
return parts.map((part, i) =>
regex.test(part) ? (
<mark
key={i}
key={`${part}-${i}`}
className="bg-yellow-300 dark:bg-yellow-700 text-inherit rounded-sm px-0.5"
>
{part}
Expand Down Expand Up @@ -648,6 +648,14 @@ export function LogViewer(props: LogViewerProps) {
const [olderLogs, setOlderLogs] = useState<unknown[]>([]);
const [isLoadingOlder, setIsLoadingOlder] = useState(false);
const [olderHasMore, setOlderHasMore] = useState(true);
const [prevSelectedServerId, setPrevSelectedServerId] =
useState(selectedServerId);

if (selectedServerId !== prevSelectedServerId) {
setPrevSelectedServerId(selectedServerId);
setOlderLogs([]);
setOlderHasMore(true);
}

const servers = props.variant === "service-logs" ? props.servers : undefined;

Expand Down Expand Up @@ -688,11 +696,6 @@ export function LogViewer(props: LogViewerProps) {
const hasMore =
olderLogs.length === 0 ? data?.hasMore || false : olderHasMore;

useEffect(() => {
setOlderLogs([]);
setOlderHasMore(true);
}, [selectedServerId]);

const loadOlderLogs = async () => {
if (isLoadingOlder || !hasMore) return;

Expand Down Expand Up @@ -974,12 +977,12 @@ export function LogViewer(props: LogViewerProps) {
</Empty>
) : (
<div className="p-4 py-2">
{filteredLogs.map((entry, idx) => {
{filteredLogs.map((entry) => {
if (props.variant === "service-logs") {
const e = entry as ServiceLogEntry;
return (
<ServiceLogRow
key={`${e.id}-${idx}`}
key={e.id}
entry={e}
search={search}
/>
Expand All @@ -989,7 +992,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as RequestEntry;
return (
<RequestRow
key={`${e.id}-${idx}`}
key={e.id}
entry={e}
search={search}
/>
Expand All @@ -999,7 +1002,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as ServerLogEntry;
return (
<ServerLogRow
key={`${e.id}-${idx}`}
key={e.id}
entry={e}
search={search}
/>
Expand All @@ -1009,7 +1012,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as BuildLogEntry;
return (
<BuildLogRow
key={`${e.timestamp}-${idx}`}
key={e.timestamp}
entry={e}
search={search}
/>
Expand All @@ -1018,7 +1021,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as BuildLogEntry;
return (
<BuildLogRow
key={`${e.timestamp}-${idx}`}
key={e.timestamp}
entry={e}
search={search}
/>
Expand Down
2 changes: 1 addition & 1 deletion web/components/service/details/pending-changes-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const PendingChangesBanner = memo(function PendingChangesBanner({
<div className="mt-2 space-y-1.5">
{changes.map((change, i) => (
<div
key={`change-${change.field}-${i}`}
key={change.field}
className="flex items-center gap-2 text-sm"
>
<span className="font-medium shrink-0 text-muted-foreground">
Expand Down
2 changes: 1 addition & 1 deletion web/components/service/details/rollout-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function StatusBadge({ status }: { status: RolloutStatus }) {

return (
<span
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium ${config.color} bg-current/10`}
className={`inline-flex items-center justify-center gap-1.5 w-28 px-2 py-1 rounded-md text-xs font-medium ${config.color} bg-current/10`}
>
<Icon className={`size-3.5 ${isAnimated ? "animate-spin" : ""}`} />
{config.label}
Expand Down
2 changes: 1 addition & 1 deletion web/components/service/details/secrets-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export const SecretsSection = memo(function SecretsSection({
)}
{pendingVars.map((variable, index) => (
<div
key={`${variable.key}-${index}`}
key={variable.key}
className="flex items-center justify-between px-3 py-2 rounded-md text-sm bg-green-500/10 border border-green-500/20"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
Expand Down
Loading
Loading