Compare commits

...

11 Commits

Author SHA1 Message Date
Owen Schwartz
7fa1180d10 Merge pull request #3221 from fosrl/dev
1.19.0-rc.1
2026-06-04 15:45:27 -07:00
Owen
769d36e289 Fix http resources not being pulled 2026-06-04 15:36:25 -07:00
Owen
a7a41b820e Add missing sshAccess key 2026-06-04 15:20:52 -07:00
Owen Schwartz
8b50f1fb65 Merge pull request #3218 from fosrl/dev
Fix installer
2026-06-04 11:21:59 -07:00
Owen
2d78a4b628 Fix installer 2026-06-04 11:21:40 -07:00
Owen Schwartz
527d4cc777 Merge pull request #3215 from fosrl/dev
1.19.0-rc.0
2026-06-04 10:34:20 -07:00
Owen Schwartz
01361884eb Potential fix for pull request finding 'CodeQL / Insecure randomness'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-04 10:33:15 -07:00
Owen
6c4cbcab5d Fix eslint errors 2026-06-04 10:22:29 -07:00
Owen Schwartz
aac25f0a53 Merge pull request #3214 from marcschaeferger/dev
Prevent cross-org site binding in target create/update
2026-06-04 10:11:53 -07:00
Marc Schäfer
f617f93a94 test(middleware): add regression tests for cross-org site binding prevention
Test the org-match logic in verifySiteAccess:
- Same org: allowed
- Cross-org: rejected with 403
- No prior org context (site-only routes): check skipped, normal flow

Test route stack ordering:
- verifySiteAccess runs after verifyResourceAccess/verifyTargetAccess
- verifySiteAccess runs before the target create/update handler

Test security scenarios for both WireGuard and newt site types.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
2026-05-29 22:57:39 +00:00
Marc Schäfer
51629247a5 fix(middleware): prevent cross-org site binding in target create/update
Extend verifySiteAccess to check that when req.userOrgId is already set
by a prior middleware (e.g. verifyResourceAccess/verifyTargetAccess), the
site from req.body.siteId belongs to the same organization. This prevents
the cross-organization tunnel boundary bypass where an attacker with
resource access in one org binds that resource's target to a site in
another org.

Add verifySiteAccess to both target route stacks:
- PUT /resource/:resourceId/target (after verifyResourceAccess)
- POST /target/:targetId (after verifyTargetAccess)

The org-match check runs before req.userOrg is overwritten, so the
resource's organization context is preserved for comparison.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
2026-05-29 22:44:16 +00:00
19 changed files with 398 additions and 68 deletions

View File

@@ -38,7 +38,5 @@ flags:
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{if .IsPostgreSQL}} {{if .IsPostgreSQL}}postgres:
postgres: connection_string: postgresql://pangolin:{{.IsPostgreSQLPass}}@postgres:5432/pangolin{{end}}
connection_string: postgresql://pangolin:{{.IsPostgreSQLPass}}@postgres:5432/pangolin
{{end}}

View File

@@ -7,23 +7,17 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 1g memory: 2g
reservations: reservations:
memory: 256m memory: 512m
{{if or .IsPostgreSQL .IsRedis}} {{if or .IsPostgreSQL .IsRedis}}depends_on:
depends_on: {{if .IsPostgreSQL}}postgres:
{{if .IsPostgreSQL}} condition: service_healthy{{end}}
postgres: {{if .IsRedis}}redis:
condition: service_healthy condition: service_healthy{{end}}
{{end}}
{{if .IsRedis}}
redis:
condition: service_healthy
{{end}}
networks: networks:
- default - default
- backend - backend{{end}}
{{end}}
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
@@ -31,8 +25,8 @@ services:
interval: "10s" interval: "10s"
timeout: "10s" timeout: "10s"
retries: 15 retries: 15
{{if .InstallGerbil}}
gerbil: {{if .InstallGerbil}}gerbil:
image: docker.io/fosrl/gerbil:{{.GerbilVersion}} image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
container_name: gerbil container_name: gerbil
restart: unless-stopped restart: unless-stopped
@@ -53,17 +47,16 @@ services:
- 21820:21820/udp - 21820:21820/udp
- 443:443 - 443:443
- 443:443/udp # For http3 QUIC if desired - 443:443/udp # For http3 QUIC if desired
- 80:80 - 80:80{{end}}
{{end}}
traefik: traefik:
image: docker.io/traefik:v3.6 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}}
ports: ports:
- 443:443 - 443:443
- 80:80 - 80:80{{end}}
{{end}}
depends_on: depends_on:
pangolin: pangolin:
condition: service_healthy condition: service_healthy
@@ -74,8 +67,7 @@ services:
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
{{if .IsPostgreSQL}} {{if .IsPostgreSQL}}postgres:
postgres:
image: postgres:18 image: postgres:18
container_name: postgres container_name: postgres
restart: unless-stopped restart: unless-stopped
@@ -91,11 +83,9 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks: networks:
- backend - backend{{end}}
{{end}}
{{if .IsRedis}} {{if .IsRedis}}redis:
redis:
image: redis:8-trixie image: redis:8-trixie
container_name: redis container_name: redis
restart: unless-stopped restart: unless-stopped
@@ -113,17 +103,14 @@ services:
retries: 3 retries: 3
start_period: 10s start_period: 10s
networks: networks:
- backend - backend{{end}}
{{end}}
networks: networks:
default: default:
driver: bridge driver: bridge
name: pangolin_frontend name: pangolin_frontend
{{if .EnableIPv6}} enable_ipv6: true{{end}} {{if .EnableIPv6}} enable_ipv6: true{{end}}
{{if or .IsPostgreSQL .IsRedis}} {{if or .IsPostgreSQL .IsRedis}} backend:
backend:
driver: bridge driver: bridge
name: pangolin_backend name: pangolin_backend
internal: true internal: true{{end}}
{{end}}

View File

@@ -1,6 +1,4 @@
{{if .IsRedis}} {{if .IsRedis}}redis:
redis:
host: "redis" host: "redis"
port: 6379 port: 6379
password: "{{.IsRedisPass}}" password: "{{.IsRedisPass}}"{{end}}
{{end}}

View File

@@ -71,9 +71,12 @@ const (
Undefined SupportedContainer = "undefined" Undefined SupportedContainer = "undefined"
) )
var redisFlag *bool
func main() { func main() {
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt") crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
redisFlag = flag.Bool("redis", false, "Install Redis as cacheing solution. Required for HA. Not required for the Enterprise version.")
flag.Parse() flag.Parse()
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking // print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
@@ -491,13 +494,13 @@ func collectUserInput() Config {
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.") config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
if config.IsEnterprise { if config.IsEnterprise {
config.IsRedis = readBool("Do you want to run the Redis containers locally? Required for HA.") if *redisFlag {
if config.IsRedis { config.IsRedis = true
config.IsRedisPass = readPassword("Enter a unique password for the Redis service.") config.IsRedisPass = readPassword("Enter a unique password for the Redis service.")
} }
} }
config.IsPostgreSQL = readBool("Do you want to run the PostgreSQL containers locally? Otherwise, default to the local SQLite database only.", false) config.IsPostgreSQL = readBool("Do you want to use PostgreSQL (not recommended for most users)?", false)
if config.IsPostgreSQL { if config.IsPostgreSQL {
config.IsPostgreSQLPass = readPassword("Enter a unique password for the PostgreSQL pangolin user.") config.IsPostgreSQLPass = readPassword("Enter a unique password for the PostgreSQL pangolin user.")
} }
@@ -544,7 +547,7 @@ func collectUserInput() Config {
fmt.Println("\n=== Advanced Configuration ===") fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true) config.EnableIPv6 = readBool("Is your server IPv6 capable?", true)
config.EnableMaxMind = readBool("Do you want to download the MaxMind GeoLite2 Country and ADN databases for blocking functionality?", true) config.EnableMaxMind = readBool("Do you want to download the MaxMind GeoLite2 Country and ASN databases for blocking functionality?", true)
if config.DashboardDomain == "" { if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required") fmt.Println("Error: Dashboard Domain name is required")

View File

@@ -2046,6 +2046,7 @@
"requireDeviceApproval": "Require Device Approvals", "requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.", "requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
"sshSettings": "SSH Settings", "sshSettings": "SSH Settings",
"sshAccess": "SSH Access",
"rdpSettings": "RDP Settings", "rdpSettings": "RDP Settings",
"vncSettings": "VNC Settings", "vncSettings": "VNC Settings",
"sshServer": "SSH Server", "sshServer": "SSH Server",

View File

@@ -665,7 +665,7 @@ export async function generateSubnetProxyTargetV2(
return; return;
} }
let targets: SubnetProxyTargetV2[] = []; const targets: SubnetProxyTargetV2[] = [];
const portRange = [ const portRange = [
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),

View File

@@ -44,7 +44,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true // UNUSED BUT USED IN PRIVATE allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
allowBrowserGatewayResources = true
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
@@ -240,7 +241,7 @@ export async function getTraefikConfig(
continue; continue;
} }
if (resource.http) { if (resource.mode === "http") {
if (!resource.domainId || !resource.fullDomain) { if (!resource.domainId || !resource.fullDomain) {
continue; continue;
} }
@@ -572,7 +573,7 @@ export async function getTraefikConfig(
serviceName serviceName
].loadBalancer.serversTransport = transportName; ].loadBalancer.serversTransport = transportName;
} }
} else { } else if (resource.mode === "tcp" || resource.mode === "udp") {
// Non-HTTP (TCP/UDP) configuration // Non-HTTP (TCP/UDP) configuration
if (!resource.enableProxy || !resource.proxyPort) { if (!resource.enableProxy || !resource.proxyPort) {
continue; continue;

View File

@@ -0,0 +1,322 @@
import { assertEquals } from "@test/assert";
/**
* Tests for the cross-organization site binding prevention in verifySiteAccess.
*
* verifySiteAccess now includes a check: if req.userOrgId is already set by a
* previous middleware (e.g. verifyResourceAccess or verifyTargetAccess), and the
* loaded site's orgId differs from req.userOrgId, the request is rejected with
* 403 Forbidden.
*
* Route stacks after fix:
* PUT /resource/:resourceId/target
* → verifyResourceAccess → verifySiteAccess → verifyLimits → ...
* POST /target/:targetId
* → verifyTargetAccess → verifySiteAccess → verifyLimits → ...
*
* verifyResourceAccess sets req.userOrgId to the resource's org.
* verifyTargetAccess sets req.userOrgId to the target's resource org.
* verifySiteAccess then checks site.orgId against req.userOrgId before
* overwriting it with the site's org.
*/
// --- Core org-matching logic (mirrors the check in verifySiteAccess) ---
function siteOrgMatchesExpectedOrg(
siteOrgId: string | null | undefined,
expectedOrgId: string | null | undefined
): boolean {
if (!siteOrgId || !expectedOrgId) {
return false;
}
return siteOrgId === expectedOrgId;
}
// Simulates the condition check in verifySiteAccess:
// if (req.userOrgId && site.orgId !== req.userOrgId) { reject }
function shouldRejectCrossOrgSite(
siteOrgId: string,
reqUserOrgId: string | undefined
): boolean {
// The actual check in verifySiteAccess is:
// if (req.userOrgId && site.orgId !== req.userOrgId) { reject }
return !!(reqUserOrgId && siteOrgId !== reqUserOrgId);
}
// --- Tests ---
function testSiteOrgMatchLogic() {
console.log("Running verifySiteAccess org-match logic tests...");
// Test 1: Same org — should match
{
const result = siteOrgMatchesExpectedOrg(
"org-attacker",
"org-attacker"
);
assertEquals(result, true, "Same org should match");
}
// Test 2: Different org — should NOT match (cross-org bypass scenario)
{
const result = siteOrgMatchesExpectedOrg("org-victim", "org-attacker");
assertEquals(
result,
false,
"Cross-org site should NOT match expected org"
);
}
// Test 3: Site orgId is null — should NOT match
{
const result = siteOrgMatchesExpectedOrg(null, "org-attacker");
assertEquals(result, false, "Null site orgId should NOT match");
}
// Test 4: Expected orgId is null — should NOT match
{
const result = siteOrgMatchesExpectedOrg("org-attacker", null);
assertEquals(result, false, "Null expected orgId should NOT match");
}
// Test 5: Both null — should NOT match
{
const result = siteOrgMatchesExpectedOrg(null, null);
assertEquals(result, false, "Both null should NOT match");
}
// Test 6: Empty string orgIds — should NOT match (empty string is falsy)
{
const result = siteOrgMatchesExpectedOrg("", "org-attacker");
assertEquals(result, false, "Empty site orgId should NOT match");
}
// Test 7: Undefined orgIds — should NOT match
{
const result = siteOrgMatchesExpectedOrg(undefined, "org-attacker");
assertEquals(result, false, "Undefined site orgId should NOT match");
}
console.log("All verifySiteAccess org-match logic tests passed.");
}
function testShouldRejectCrossOrgSite() {
console.log(
"Running shouldRejectCrossOrgSite tests (mirrors verifySiteAccess check)..."
);
// Test: No prior org context (undefined) — should NOT reject
// This is the normal case for site-only routes (e.g. PUT /site/:siteId)
// where verifySiteAccess runs without a prior verifyResourceAccess.
{
const shouldReject = shouldRejectCrossOrgSite("org-victim", undefined);
assertEquals(
shouldReject,
false,
"No prior org context should NOT reject (normal site routes)"
);
}
// Test: Same org — should NOT reject
{
const shouldReject = shouldRejectCrossOrgSite(
"org-attacker",
"org-attacker"
);
assertEquals(shouldReject, false, "Same org should NOT reject");
}
// Test: Different org — should reject
{
const shouldReject = shouldRejectCrossOrgSite(
"org-victim",
"org-attacker"
);
assertEquals(shouldReject, true, "Cross-org site should be rejected");
}
// Test: Empty string userOrgId — should NOT reject (falsy, check is skipped)
{
const shouldReject = shouldRejectCrossOrgSite("org-victim", "");
assertEquals(
shouldReject,
false,
"Empty string userOrgId should NOT reject (check is skipped)"
);
}
console.log("All shouldRejectCrossOrgSite tests passed.");
}
// --- Route stack validation tests ---
function testRouteStackOrdering() {
console.log("Running route stack ordering tests...");
const createTargetStack = [
"verifyResourceAccess",
"verifySiteAccess",
"verifyLimits",
"verifyUserHasAction",
"logActionAudit",
"createTarget"
];
const updateTargetStack = [
"verifyTargetAccess",
"verifySiteAccess",
"verifyLimits",
"verifyUserHasAction",
"logActionAudit",
"updateTarget"
];
// Verify verifySiteAccess comes after resource/target access middleware
{
const siteAccessIndex = createTargetStack.indexOf("verifySiteAccess");
const resourceAccessIndex = createTargetStack.indexOf(
"verifyResourceAccess"
);
assertEquals(
siteAccessIndex > resourceAccessIndex,
true,
"verifySiteAccess must come after verifyResourceAccess in create target stack"
);
}
{
const siteAccessIndex = updateTargetStack.indexOf("verifySiteAccess");
const targetAccessIndex =
updateTargetStack.indexOf("verifyTargetAccess");
assertEquals(
siteAccessIndex > targetAccessIndex,
true,
"verifySiteAccess must come after verifyTargetAccess in update target stack"
);
}
// Verify verifySiteAccess comes before the handler
{
const siteAccessIndex = createTargetStack.indexOf("verifySiteAccess");
const handlerIndex = createTargetStack.indexOf("createTarget");
assertEquals(
siteAccessIndex < handlerIndex,
true,
"verifySiteAccess must come before createTarget handler"
);
}
{
const siteAccessIndex = updateTargetStack.indexOf("verifySiteAccess");
const handlerIndex = updateTargetStack.indexOf("updateTarget");
assertEquals(
siteAccessIndex < handlerIndex,
true,
"verifySiteAccess must come before updateTarget handler"
);
}
console.log("All route stack ordering tests passed.");
}
// --- Security scenario tests ---
function testSecurityScenarios() {
console.log("Running security scenario tests...");
// Scenario 1: Attacker has resource access in org_attacker, but tries to
// bind target to a site in org_victim.
// verifyResourceAccess passes (sets req.userOrgId = "org_attacker").
// verifySiteAccess loads site (org_victim), checks site.orgId !== req.userOrgId.
// Expected: 403 Forbidden.
{
const shouldReject = shouldRejectCrossOrgSite(
"org_victim",
"org_attacker"
);
assertEquals(
shouldReject,
true,
"Scenario 1: Cross-org site binding must be rejected"
);
}
// Scenario 2: Attacker has resource access AND site access in another org.
// Even though the user has site access, verifySiteAccess rejects because
// the org-match check runs before the site access check.
// Expected: 403 Forbidden (org mismatch caught before site access check).
{
const shouldReject = shouldRejectCrossOrgSite(
"org_victim",
"org_attacker"
);
assertEquals(
shouldReject,
true,
"Scenario 2: Cross-org site must be rejected even if user has site access"
);
}
// Scenario 3: Legitimate user creates target with site in same org.
// verifyResourceAccess passes, verifySiteAccess org-match passes (same org),
// verifySiteAccess site access passes.
// Expected: 201 Created.
{
const shouldReject = shouldRejectCrossOrgSite(
"org_attacker",
"org_attacker"
);
assertEquals(
shouldReject,
false,
"Scenario 3: Same-org site must be allowed"
);
}
// Scenario 4: WireGuard site in victim org — org mismatch is caught before
// any DB write, pickPort, addPeer, or addTargets side effect.
{
const shouldReject = shouldRejectCrossOrgSite(
"org_victim",
"org_attacker"
);
assertEquals(
shouldReject,
true,
"Scenario 4: WireGuard cross-org site must be rejected before addPeer"
);
}
// Scenario 5: Newt site in victim org — same as scenario 4 but for newt.
{
const shouldReject = shouldRejectCrossOrgSite(
"org_victim",
"org_attacker"
);
assertEquals(
shouldReject,
true,
"Scenario 5: Newt cross-org site must be rejected before addTargets"
);
}
// Scenario 6: Normal site-only route (e.g. PUT /site/:siteId) where
// verifySiteAccess runs without a prior verifyResourceAccess.
// req.userOrgId is undefined, so the org-match check is skipped.
// Normal site access verification proceeds.
{
const shouldReject = shouldRejectCrossOrgSite("org_victim", undefined);
assertEquals(
shouldReject,
false,
"Scenario 6: Site-only routes should skip org-match check"
);
}
console.log("All security scenario tests passed.");
}
// Run all tests
testSiteOrgMatchLogic();
testShouldRejectCrossOrgSite();
testRouteStackOrdering();
testSecurityScenarios();

View File

@@ -71,6 +71,15 @@ export async function verifySiteAccess(
); );
} }
if (req.userOrgId && site.orgId !== req.userOrgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this site"
)
);
}
if (!req.userOrg) { if (!req.userOrg) {
// Get user's role ID in the organization // Get user's role ID in the organization
const userOrgRole = await db const userOrgRole = await db
@@ -128,10 +137,7 @@ export async function verifySiteAccess(
.where( .where(
and( and(
eq(roleSites.siteId, site.siteId), eq(roleSites.siteId, site.siteId),
inArray( inArray(roleSites.roleId, req.userOrgRoleIds!)
roleSites.roleId,
req.userOrgRoleIds!
)
) )
) )
.limit(1) .limit(1)

View File

@@ -493,16 +493,29 @@ export async function getTraefikConfig(
const transportName = `${key}-transport`; const transportName = `${key}-transport`;
const headersMiddlewareName = `${key}-headers-middleware`; const headersMiddlewareName = `${key}-headers-middleware`;
logger.debug(
`Processing resource ${resource.name} with domain ${fullDomain} and ${targets.length} targets`
);
if (!resource.enabled) { if (!resource.enabled) {
logger.debug(
`Resource ${resource.name} is disabled, skipping Traefik config`
);
continue; continue;
} }
if (resource.http) { if (resource.mode == "http") {
if (!resource.domainId) { if (!resource.domainId) {
logger.debug(
`Resource ${resource.name} does not have a domainId, skipping Traefik config`
);
continue; continue;
} }
if (!resource.fullDomain) { if (!resource.fullDomain) {
logger.debug(
`Resource ${resource.name} does not have a fullDomain, skipping Traefik config`
);
continue; continue;
} }
@@ -958,7 +971,7 @@ export async function getTraefikConfig(
serviceName serviceName
].loadBalancer.serversTransport = transportName; ].loadBalancer.serversTransport = transportName;
} }
} else { } else if (resource.mode == "tcp" || resource.mode == "udp") {
// Non-HTTP (TCP/UDP) configuration // Non-HTTP (TCP/UDP) configuration
if (!resource.enableProxy) { if (!resource.enableProxy) {
continue; continue;

View File

@@ -12,6 +12,7 @@
*/ */
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { randomInt } from "crypto";
import { z } from "zod"; import { z } from "zod";
import { import {
actionAuditLog, actionAuditLog,
@@ -392,7 +393,7 @@ export async function signSshKey(
if (existingUserWithSameName) { if (existingUserWithSameName) {
let foundUniqueUsername = false; let foundUniqueUsername = false;
for (let attempt = 0; attempt < 20; attempt++) { for (let attempt = 0; attempt < 20; attempt++) {
const randomNum = Math.floor(Math.random() * 101); // 0 to 100 const randomNum = randomInt(0, 101); // 0 to 100
const candidateUsername = `${usernameToUse}${randomNum}`; const candidateUsername = `${usernameToUse}${randomNum}`;
const [existingUser] = await db const [existingUser] = await db

View File

@@ -561,6 +561,7 @@ authenticated.delete(
authenticated.put( authenticated.put(
"/resource/:resourceId/target", "/resource/:resourceId/target",
verifyResourceAccess, verifyResourceAccess,
verifySiteAccess,
verifyLimits, verifyLimits,
verifyUserHasAction(ActionsEnum.createTarget), verifyUserHasAction(ActionsEnum.createTarget),
logActionAudit(ActionsEnum.createTarget), logActionAudit(ActionsEnum.createTarget),
@@ -612,6 +613,7 @@ authenticated.get(
authenticated.post( authenticated.post(
"/target/:targetId", "/target/:targetId",
verifyTargetAccess, verifyTargetAccess,
verifySiteAccess,
verifyLimits, verifyLimits,
verifyUserHasAction(ActionsEnum.updateTarget), verifyUserHasAction(ActionsEnum.updateTarget),
logActionAudit(ActionsEnum.updateTarget), logActionAudit(ActionsEnum.updateTarget),
@@ -1234,7 +1236,8 @@ export const authRouter = Router();
unauthenticated.use("/auth", authRouter); unauthenticated.use("/auth", authRouter);
authRouter.use( authRouter.use(
rateLimit({ rateLimit({
windowMs: config.getRawConfig().rate_limits.auth.window_minutes * 60 * 1000, windowMs:
config.getRawConfig().rate_limits.auth.window_minutes * 60 * 1000,
max: config.getRawConfig().rate_limits.auth.max_requests, max: config.getRawConfig().rate_limits.auth.max_requests,
keyGenerator: (req) => keyGenerator: (req) =>
`authRouterGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`, `authRouterGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`,

View File

@@ -103,7 +103,7 @@ export function ProxyResourceTargetsForm({
// Notify parent of changes (create mode) // Notify parent of changes (create mode)
useEffect(() => { useEffect(() => {
onChange?.(targets); onChange?.(targets);
}, [targets]); // eslint-disable-line react-hooks/exhaustive-deps }, [targets]);
// Poll health status only in edit mode // Poll health status only in edit mode
const { data: polledTargets } = useQuery({ const { data: polledTargets } = useQuery({

View File

@@ -86,7 +86,7 @@ export default async function Page(props: {
targetOrgId = lastOrgCookie; targetOrgId = lastOrgCookie;
} else { } else {
let ownedOrg = orgs.find((org) => org.isOwner); let ownedOrg = orgs.find((org) => org.isOwner);
let primaryOrg = orgs.find((org) => org.isPrimaryOrg); const primaryOrg = orgs.find((org) => org.isPrimaryOrg);
if (!ownedOrg) { if (!ownedOrg) {
if (primaryOrg) { if (primaryOrg) {
ownedOrg = primaryOrg; ownedOrg = primaryOrg;

View File

@@ -16,9 +16,9 @@ export const metadata: Metadata = {
export default async function MaintenanceScreen() { export default async function MaintenanceScreen() {
const t = await getTranslations(); const t = await getTranslations();
let title = t("privateMaintenanceScreenTitle"); const title = t("privateMaintenanceScreenTitle");
let message = t("privateMaintenanceScreenMessage"); const message = t("privateMaintenanceScreenMessage");
let steps = t("privateMaintenanceScreenSteps"); const steps = t("privateMaintenanceScreenSteps");
return ( return (
<div className="min-h-screen flex items-center justify-center p-4"> <div className="min-h-screen flex items-center justify-center p-4">

View File

@@ -17,7 +17,7 @@ export default async function RdpPage() {
const hostname = host.split(":")[0]; const hostname = host.split(":")[0];
let target: GetBrowserTargetResponse | null = null; let target: GetBrowserTargetResponse | null = null;
let error: string | null = null; const error: string | null = null;
try { try {
const res = await priv.get<AxiosResponse<GetBrowserTargetResponse>>( const res = await priv.get<AxiosResponse<GetBrowserTargetResponse>>(

View File

@@ -180,7 +180,6 @@ export default function SshClient({
certificate: signedKeyData.certificate certificate: signedKeyData.certificate
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
function connect(override?: ConnectCredentials) { function connect(override?: ConnectCredentials) {

View File

@@ -39,7 +39,6 @@ export default function VncClient({
}); });
const [connected, setConnected] = useState(false); const [connected, setConnected] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rfbRef = useRef<any>(null); const rfbRef = useRef<any>(null);
const screenRef = useRef<HTMLDivElement>(null); const screenRef = useRef<HTMLDivElement>(null);
@@ -59,7 +58,7 @@ export default function VncClient({
// Clean up on unmount. // Clean up on unmount.
useEffect(() => { useEffect(() => {
return () => disconnect(); return () => disconnect();
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []);
const connect = async () => { const connect = async () => {
if (!target) { if (!target) {
@@ -115,7 +114,6 @@ export default function VncClient({
options.credentials = { password: form.password }; options.credentials = { password: form.password };
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rfb: any = new RFB(screenRef.current, wsUrl, options); const rfb: any = new RFB(screenRef.current, wsUrl, options);
rfb.scaleViewport = true; rfb.scaleViewport = true;

View File

@@ -17,7 +17,7 @@ export default async function VncPage() {
const hostname = host.split(":")[0]; const hostname = host.split(":")[0];
let target: GetBrowserTargetResponse | null = null; let target: GetBrowserTargetResponse | null = null;
let error: string | null = null; const error: string | null = null;
try { try {
const res = await priv.get<AxiosResponse<GetBrowserTargetResponse>>( const res = await priv.get<AxiosResponse<GetBrowserTargetResponse>>(