Browse Source

Merge pull request #15257 from thingsboard/master-rc-merge

Merge rc to master
pull/15269/head
Viacheslav Klimov 3 months ago
committed by GitHub
parent
commit
4d5d61664a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 64
      application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersCustomizer.java
  3. 56
      application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersProperties.java
  4. 12
      application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java
  5. 19
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  6. 11
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  8. 75
      application/src/main/resources/thingsboard.yml
  9. 56
      common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
  10. 242
      common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
  11. 8
      dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
  12. 12
      msa/js-executor/yarn.lock
  13. 14
      msa/web-ui/config/custom-environment-variables.yml
  14. 14
      msa/web-ui/config/default.yml
  15. 30
      msa/web-ui/server.ts
  16. 12
      msa/web-ui/yarn.lock
  17. 17
      pom.xml
  18. 174
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java
  19. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  20. 161
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java
  21. 32
      ui-ngx/package.json
  22. 0
      ui-ngx/patches/@angular+build+20.3.20.patch
  23. 4
      ui-ngx/patches/@angular+core+20.3.18.patch
  24. 49
      ui-ngx/src/app/core/ws/websocket.service.ts
  25. 372
      ui-ngx/yarn.lock

10
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -622,11 +622,21 @@ public class ActorSystemContext {
@Value("${actors.rule.external.ssrf_additional_blocked_hosts:}")
private List<String> ssrfAdditionalBlockedHosts;
@Value("${actors.rule.external.ssrf_allowed_hosts:}")
private List<String> ssrfAllowedHosts;
@PostConstruct
public void init() {
this.localCacheType = "caffeine".equals(cacheType);
SsrfProtectionValidator.setEnabled(ssrfProtectionEnabled);
SsrfProtectionValidator.setAdditionalBlockedHosts(ssrfAdditionalBlockedHosts);
SsrfProtectionValidator.setAllowedHosts(ssrfAllowedHosts);
if (!ssrfProtectionEnabled) {
log.warn("SSRF protection for external rule nodes is DISABLED. This allows rule chains to make HTTP requests to " +
"internal/private network addresses including cloud metadata endpoints. It is strongly recommended to " +
"enable SSRF protection by setting SSRF_PROTECTION_ENABLED=true. If your rule chains need to access " +
"devices on local networks, use SSRF_ALLOWED_HOSTS to whitelist specific addresses or ranges.");
}
}
@Value("${actors.tenant.create_components_on_init:true}")

64
application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersCustomizer.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Slf4j
@Component
@RequiredArgsConstructor
public class HttpSecurityHeadersCustomizer {
private final HttpSecurityHeadersProperties properties;
public void customize(HeadersConfigurer<?> headers) {
if (properties.getXContentTypeOptions().isEnabled()) {
headers.contentTypeOptions(config -> {});
}
if (properties.getReferrerPolicy().isEnabled()) {
headers.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", properties.getReferrerPolicy().getValue()));
}
if (properties.getXFrameOptions().isEnabled()) {
String value = properties.getXFrameOptions().getValue();
if ("DENY".equalsIgnoreCase(value)) {
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny);
} else {
if (!"SAMEORIGIN".equalsIgnoreCase(value)) {
log.warn("Unrecognized X-Frame-Options value '{}', falling back to SAMEORIGIN. Valid values: DENY, SAMEORIGIN", value);
}
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin);
}
}
if (properties.getContentSecurityPolicy().isEnabled() && StringUtils.hasText(properties.getContentSecurityPolicy().getValue())) {
headers.contentSecurityPolicy(csp -> {
csp.policyDirectives(properties.getContentSecurityPolicy().getValue());
if (properties.getContentSecurityPolicy().isReportOnly()) {
csp.reportOnly();
}
});
}
}
}

56
application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersProperties.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "security.headers")
@Data
public class HttpSecurityHeadersProperties {
private XContentTypeOptions xContentTypeOptions = new XContentTypeOptions();
private ReferrerPolicy referrerPolicy = new ReferrerPolicy();
private XFrameOptions xFrameOptions = new XFrameOptions();
private ContentSecurityPolicy contentSecurityPolicy = new ContentSecurityPolicy();
@Data
public static class XContentTypeOptions {
private boolean enabled = true;
}
@Data
public static class ReferrerPolicy {
private boolean enabled = true;
private String value = "strict-origin-when-cross-origin";
}
@Data
public static class XFrameOptions {
private boolean enabled = false;
private String value = "SAMEORIGIN";
}
@Data
public static class ContentSecurityPolicy {
private boolean enabled = false;
private String value = "";
private boolean reportOnly = false;
}
}

12
application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
@ -33,11 +34,16 @@ import org.springframework.security.web.SecurityFilterChain;
@ConditionalOnExpression("'${service.type:null}'=='tb-rule-engine'")
public class TbRuleEngineSecurityConfiguration {
@Autowired
private HttpSecurityHeadersCustomizer httpSecurityHeadersCustomizer;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.cacheControl(config -> {})
.frameOptions(config -> {}).disable())
http.headers(headers -> {
headers.defaultsDisabled();
headers.cacheControl(config -> {});
httpSecurityHeadersCustomizer.customize(headers);
})
.cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(config -> config

19
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -143,6 +143,9 @@ public class ThingsboardSecurityConfiguration {
@Autowired
private AuthExceptionHandler authExceptionHandler;
@Autowired
private HttpSecurityHeadersCustomizer httpSecurityHeadersCustomizer;
@Bean
protected PayloadSizeFilter payloadSizeFilter() {
return new PayloadSizeFilter(maxPayloadSizeConfig);
@ -231,9 +234,11 @@ public class ThingsboardSecurityConfiguration {
http
.securityMatchers(matchers -> matchers
.requestMatchers("/*.js", "/*.css", "/*.ico", "/assets/**", "/static/**"))
.headers(header -> header
.defaultsDisabled()
.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public")))
.headers(headers -> {
headers.defaultsDisabled();
headers.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public"));
httpSecurityHeadersCustomizer.customize(headers);
})
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache(RequestCacheConfigurer::disable)
.securityContext(AbstractHttpConfigurer::disable)
@ -243,8 +248,12 @@ public class ThingsboardSecurityConfiguration {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers.defaultsDisabled()
.crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN)))
http.headers(headers -> {
headers.defaultsDisabled();
headers.cacheControl(config -> {});
headers.crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN));
httpSecurityHeadersCustomizer.customize(headers);
})
.cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(config -> {})

11
application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java

@ -29,6 +29,7 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
@ -109,10 +110,13 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
adaptiveCard.getActions().add(actionOpenUrl);
}
URI webhookUri = new URI(targetConfig.getWebhookUrl());
SsrfProtectionValidator.validateUri(webhookUri);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsAdaptiveCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
restTemplate.postForEntity(webhookUri, request, String.class);
}
private void sendTeamsMessageCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException, URISyntaxException {
@ -139,10 +143,13 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
teamsMessageCard.setPotentialAction(List.of(actionCard));
}
URI webhookUri = new URI(targetConfig.getWebhookUrl());
SsrfProtectionValidator.validateUri(webhookUri);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsMessageCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
restTemplate.postForEntity(webhookUri, request, String.class);
}
private String getButtonUri(MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException {

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -23,12 +23,15 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.net.URI;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service(value = "customOAuth2ClientMapper")
@ -64,6 +67,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
throw new RuntimeException("Can't convert principal to JSON string", e);
}
try {
SsrfProtectionValidator.validateUri(new URI(custom.getUrl()));
return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody();
} catch (Exception e) {
log.error("There was an error during connection to custom mapper endpoint", e);

75
application/src/main/resources/thingsboard.yml

@ -188,6 +188,57 @@ security:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}"
# The password of the cacerts keystore file
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
# HTTP security response headers configuration.
# These headers are set on responses from the ThingsBoard backend (tb-node).
# In microservice deployments, the web-ui (Express.js) has its own header configuration
# under msa/web-ui/config/ using the same environment variable names.
headers:
# X-Content-Type-Options header prevents browsers from MIME-sniffing the Content-Type.
# Safe to enable. Only disable if you intentionally serve resources with mismatched Content-Type.
x-content-type-options:
# Enable/disable X-Content-Type-Options header. Prevents browsers from MIME-sniffing the Content-Type
enabled: "${SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED:true}"
# Referrer-Policy header controls how much referrer info the browser sends with requests.
# The default 'strict-origin-when-cross-origin' matches the browser's built-in default,
# so enabling this does not change existing behavior — it just makes the policy explicit.
# Valid values: no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin,
# same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
referrer-policy:
# Enable/disable Referrer-Policy header
enabled: "${SECURITY_HEADERS_REFERRER_POLICY_ENABLED:true}"
# Referrer-Policy header value
value: "${SECURITY_HEADERS_REFERRER_POLICY_VALUE:strict-origin-when-cross-origin}"
# X-Frame-Options header protects against clickjacking attacks by preventing the page
# from being loaded in iframes on other domains.
# Disabled by default because ThingsBoard supports multi-domain deployments where
# the platform may be embedded in iframes on customer domains.
# WARNING: Enabling with DENY will block ALL iframe embedding including dashboards
# embedded on external sites. Use SAMEORIGIN to allow same-domain iframes only.
x-frame-options:
# Enable/disable X-Frame-Options header. Protects against clickjacking attacks
enabled: "${SECURITY_HEADERS_X_FRAME_OPTIONS_ENABLED:false}"
# Valid values: DENY, SAMEORIGIN
value: "${SECURITY_HEADERS_X_FRAME_OPTIONS_VALUE:SAMEORIGIN}"
# Content-Security-Policy header mitigates XSS and data injection attacks by restricting
# which resources the browser is allowed to load.
# Disabled by default because ThingsBoard supports multi-domain deployments and
# because custom HTML Card widgets may use inline scripts, inline styles, and
# external resources that a restrictive CSP would block.
# WARNING when enabling: A strict CSP (e.g. script-src 'self') will break:
# - HTML Card widgets with inline JavaScript
# - Custom widget types with inline scripts/styles
# - Widgets loading external resources (images, fonts, scripts)
# - Dashboard embedding via iframes (if frame-ancestors is restrictive)
# Use 'report-only: true' first to test the impact before enforcing.
# Example value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'"
content-security-policy:
# Enable/disable Content-Security-Policy header. Mitigates XSS and data injection attacks
enabled: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_ENABLED:false}"
# Full CSP directive string
value: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_VALUE:}"
# If true, uses Content-Security-Policy-Report-Only header instead — the browser
# reports violations but does not enforce them. Use for testing before enforcing.
report-only: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_REPORT_ONLY:false}"
# Mail settings parameters
# Configures mail service OAuth2 token refresh and per-tenant sending rate limits.
@ -536,6 +587,10 @@ actors:
# Comma-separated list of additional blocked destinations (IPs, CIDR subnets, or hostnames).
# Example: "198.51.100.0/24,metadata.tencentyun.com,rancher-metadata"
ssrf_additional_blocked_hosts: "${SSRF_ADDITIONAL_BLOCKED_HOSTS:}"
# Comma-separated list of allowed destinations that bypass SSRF blocking (IPs, CIDR subnets, or hostnames).
# Use this when your rule chains need to reach devices on private networks (e.g., 192.168.1.0/24).
# Example: "192.168.1.0/24,10.0.0.0/8,my-internal-service.corp"
ssrf_allowed_hosts: "${SSRF_ALLOWED_HOSTS:}"
rpc:
# Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
@ -826,22 +881,28 @@ updates:
# Enable/disable checks for the new version
enabled: "${UPDATES_ENABLED:true}"
# Spring CORS configuration parameters
# Defines allowed origins, methods, headers, and credentials for cross-origin REST API requests.
# Spring CORS configuration parameters.
# Controls the Access-Control-Allow-Origin and Access-Control-Allow-Credentials response headers.
# WARNING: The default configuration allows cross-origin requests from ANY domain with credentials.
# This means any website can make API requests on behalf of an authenticated user if the token
# is accessible (e.g., via XSS). For production deployments, restrict to your domain(s):
# TB_CORS_ALLOWED_ORIGIN_PATTERNS=https://your-domain.com
# For multi-domain deployments, list all allowed domains comma-separated:
# TB_CORS_ALLOWED_ORIGIN_PATTERNS=https://domain1.com,https://domain2.com
spring.mvc.cors:
mappings:
# Intercept path
"[/api/**]":
#Comma-separated list of origins to allow. '*' allows all origins. When not set, CORS support is disabled.
allowed-origin-patterns: "*"
allowed-origin-patterns: "${TB_CORS_ALLOWED_ORIGIN_PATTERNS:*}"
#Comma-separated list of methods to allow. '*' allows all methods.
allowed-methods: "*"
allowed-methods: "${TB_CORS_ALLOWED_METHODS:*}"
#Comma-separated list of headers to allow in a request. '*' allows all headers.
allowed-headers: "*"
allowed-headers: "${TB_CORS_ALLOWED_HEADERS:*}"
#How long, in seconds, the response from a pre-flight request can be cached by clients.
max-age: "1800"
max-age: "${TB_CORS_MAX_AGE:1800}"
#Set whether credentials are supported. When not set, credentials are not supported.
allow-credentials: "true"
allow-credentials: "${TB_CORS_ALLOW_CREDENTIALS:true}"
# General spring parameters
# Miscellaneous Spring Boot settings for circular references, Freemarker, MVC, multipart, and JPA dialect.

56
common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java

@ -38,6 +38,7 @@ public class SsrfProtectionValidator {
private static final Set<String> BLOCKED_HOSTNAME_SUFFIXES = Set.of(".internal", ".local");
private static volatile AdditionalBlockedHosts additionalBlocked = AdditionalBlockedHosts.EMPTY;
private static volatile AllowedHosts allowedHosts = AllowedHosts.EMPTY;
// Well-known cloud metadata endpoints not covered by the JDK checks (isLoopback, isSiteLocal, isLinkLocal)
private static final List<CidrRange> CLOUD_METADATA_RANGES = List.of(
@ -66,6 +67,13 @@ public class SsrfProtectionValidator {
}
String hostLower = host.toLowerCase();
// Allow-listed hostnames bypass all hostname and IP checks
AllowedHosts currentAllowed = allowedHosts;
if (currentAllowed.hostnames.contains(hostLower)) {
return;
}
if (BLOCKED_HOSTNAMES.contains(hostLower) || additionalBlocked.hostnames.contains(hostLower)) {
throwBlockedHost(host);
}
@ -98,7 +106,15 @@ public class SsrfProtectionValidator {
}
}
private static boolean isBlockedAddress(InetAddress address) {
public static boolean isBlockedAddress(InetAddress address) {
// Check allow-list first: allowed addresses bypass all block checks
AllowedHosts currentAllowed = allowedHosts;
for (CidrRange cidr : currentAllowed.cidrRanges) {
if (cidr.contains(address)) {
return false;
}
}
// Covers 127.0.0.0/8 and ::1
if (address.isLoopbackAddress()) {
return true;
@ -142,14 +158,37 @@ public class SsrfProtectionValidator {
throw new RuntimeException("URI is invalid: host '" + host + "' is not allowed");
}
public static boolean isEnabled() {
return enabled;
}
public static void setEnabled(boolean enabled) {
SsrfProtectionValidator.enabled = enabled;
}
public static void setAdditionalBlockedHosts(List<String> entries) {
ParsedHostEntries parsed = parseHostEntries(entries);
additionalBlocked = new AdditionalBlockedHosts(parsed.cidrRanges, parsed.hostnames);
if (!parsed.cidrRanges.isEmpty() || !parsed.hostnames.isEmpty()) {
log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", parsed.cidrRanges.size(), parsed.hostnames.size());
}
}
public static void setAllowedHosts(List<String> entries) {
ParsedHostEntries parsed = parseHostEntries(entries);
allowedHosts = new AllowedHosts(parsed.cidrRanges, parsed.hostnames);
if (!parsed.cidrRanges.isEmpty() || !parsed.hostnames.isEmpty()) {
log.info("SSRF allowed hosts configured: {} CIDR range(s), {} hostname(s)", parsed.cidrRanges.size(), parsed.hostnames.size());
}
}
public static boolean isHostnameAllowed(String hostname) {
return allowedHosts.hostnames.contains(hostname.toLowerCase());
}
private static ParsedHostEntries parseHostEntries(List<String> entries) {
if (entries == null || entries.isEmpty()) {
additionalBlocked = AdditionalBlockedHosts.EMPTY;
return;
return ParsedHostEntries.EMPTY;
}
List<CidrRange> cidrRanges = new ArrayList<>();
Set<String> hostnames = new HashSet<>();
@ -168,10 +207,9 @@ public class SsrfProtectionValidator {
hostnames.add(trimmed.toLowerCase());
}
}
additionalBlocked = new AdditionalBlockedHosts(
return new ParsedHostEntries(
Collections.unmodifiableList(cidrRanges),
Collections.unmodifiableSet(hostnames));
log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", cidrRanges.size(), hostnames.size());
}
private static boolean isIpLiteral(String entry) {
@ -179,10 +217,18 @@ public class SsrfProtectionValidator {
return !entry.isEmpty() && (Character.isDigit(entry.charAt(0)) || entry.contains(":"));
}
private record ParsedHostEntries(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final ParsedHostEntries EMPTY = new ParsedHostEntries(Collections.emptyList(), Collections.emptySet());
}
record AdditionalBlockedHosts(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final AdditionalBlockedHosts EMPTY = new AdditionalBlockedHosts(Collections.emptyList(), Collections.emptySet());
}
record AllowedHosts(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final AllowedHosts EMPTY = new AllowedHosts(Collections.emptyList(), Collections.emptySet());
}
record CidrRange(byte[] network, int prefixLength) {
static CidrRange of(String ip, int prefixLength) {

242
common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java

@ -20,10 +20,12 @@ import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.net.InetAddress;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -335,4 +337,244 @@ public class SsrfProtectionValidatorTest {
}
}
// --- Allow-list tests ---
@Test
void testAllowListCidrAllowsPrivateAddress() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
// 192.168.1.1 is normally blocked (site-local), but allow-listed
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
// Other private ranges remain blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://10.0.0.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListHostnameBypassesSuffixCheck() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("my-device.local"));
// .local suffix is normally blocked, but allow-listed hostname passes
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://my-device.local/api"), true));
// Other .local hostnames remain blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://other-device.local/api"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListPrecedenceOverBlockList() {
try {
// Block 8.8.8.0/24 via additional-blocked, but allow 8.8.8.8 via allow-list
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("8.8.8.0/24"));
SsrfProtectionValidator.setAllowedHosts(List.of("8.8.8.8"));
// Allow-list should win
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.8"), true));
// Adjacent IP still blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.9"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testIsBlockedAddressPublicApi() throws Exception {
InetAddress loopback = InetAddress.getByName("127.0.0.1");
assertThat(SsrfProtectionValidator.isBlockedAddress(loopback)).isTrue();
InetAddress publicIp = InetAddress.getByName("8.8.8.8");
assertThat(SsrfProtectionValidator.isBlockedAddress(publicIp)).isFalse();
// Allow-listed private address
try {
SsrfProtectionValidator.setAllowedHosts(List.of("10.0.0.0/8"));
InetAddress privateIp = InetAddress.getByName("10.1.2.3");
assertThat(SsrfProtectionValidator.isBlockedAddress(privateIp)).isFalse();
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testIsEnabledAccessor() {
boolean original = SsrfProtectionValidator.isEnabled();
try {
SsrfProtectionValidator.setEnabled(true);
assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
SsrfProtectionValidator.setEnabled(false);
assertThat(SsrfProtectionValidator.isEnabled()).isFalse();
} finally {
SsrfProtectionValidator.setEnabled(original);
}
}
@Test
void testSetAllowedHostsEmptyAndNull() {
// Should not throw
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
SsrfProtectionValidator.setAllowedHosts(null);
}
@Test
void testIsHostnameAllowed() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("my-device.local", "Internal-Server.Corp"));
assertThat(SsrfProtectionValidator.isHostnameAllowed("my-device.local")).isTrue();
assertThat(SsrfProtectionValidator.isHostnameAllowed("MY-DEVICE.LOCAL")).isTrue(); // case-insensitive
assertThat(SsrfProtectionValidator.isHostnameAllowed("internal-server.corp")).isTrue();
assertThat(SsrfProtectionValidator.isHostnameAllowed("other-device.local")).isFalse();
assertThat(SsrfProtectionValidator.isHostnameAllowed("example.com")).isFalse();
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testIsHostnameAllowedEmptyList() {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
assertThat(SsrfProtectionValidator.isHostnameAllowed("anything")).isFalse();
}
@Test
void testValidateUriUsesStaticEnabledFlag() {
boolean original = SsrfProtectionValidator.isEnabled();
try {
// When enabled, loopback is blocked via the public one-arg overload
SsrfProtectionValidator.setEnabled(true);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1")))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// When disabled, loopback passes
SsrfProtectionValidator.setEnabled(false);
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1")));
} finally {
SsrfProtectionValidator.setEnabled(original);
}
}
@Test
void testAllowListHostnameCaseInsensitive() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("My-Device.LOCAL"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://my-device.local/api"), true));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://MY-DEVICE.LOCAL/api"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListOverridesCloudMetadataRange() {
try {
// 169.254.169.254 is link-local (blocked by default), allow-list should override
SsrfProtectionValidator.setAllowedHosts(List.of("169.254.169.254"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://169.254.169.254/latest/meta-data/"), true));
// Other link-local still blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://169.254.1.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListOverridesLoopback() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("127.0.0.0/8"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1"), true));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.1.2.3"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListCidrBoundary() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
// Last address in range
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.255"), true));
// First address outside range
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.2.0"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Different subnet entirely
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.0.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testBlockedIpv6UniqueLocal() throws Exception {
// fc00::/7 covers fc00:: through fdff::
InetAddress fc00 = InetAddress.getByName("fc00::1");
assertThat(SsrfProtectionValidator.isBlockedAddress(fc00)).isTrue();
InetAddress fdAddr = InetAddress.getByName("fd12:3456:789a::1");
assertThat(SsrfProtectionValidator.isBlockedAddress(fdAddr)).isTrue();
// fe00:: is NOT in fc00::/7 (it's in fe80::/10 link-local, but fe00:: without the 80 bits is different)
// 2001:db8:: is a public documentation prefix, not blocked
InetAddress publicV6 = InetAddress.getByName("2001:db8::1");
assertThat(SsrfProtectionValidator.isBlockedAddress(publicV6)).isFalse();
}
@Test
void testParseHostEntriesWithWhitespaceAndBlanks() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of(" 192.168.1.0/24 ", "", " ", "my-host.corp"));
// Trimmed CIDR works
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
// Trimmed hostname works
assertThat(SsrfProtectionValidator.isHostnameAllowed("my-host.corp")).isTrue();
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testSetAllowedHostsReplacePrevious() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
// Replace with different range
SsrfProtectionValidator.setAllowedHosts(List.of("10.0.0.0/8"));
// Old range no longer allowed
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// New range allowed
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://10.1.2.3"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListHostnameBypassesBlockedHostname() {
try {
// "localhost" is in BLOCKED_HOSTNAMES; allow-listing it should let it through
SsrfProtectionValidator.setAllowedHosts(List.of("localhost"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://localhost/path"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
}

8
dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.MapperType;
@ -28,6 +29,8 @@ import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import java.net.URI;
@Component
@AllArgsConstructor
public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> {
@ -64,6 +67,11 @@ public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> {
if (StringUtils.isEmpty(customConfig.getUrl())) {
throw new DataValidationException("Custom mapper URL should be specified!");
}
try {
SsrfProtectionValidator.validateUri(new URI(customConfig.getUrl()));
} catch (Exception e) {
throw new DataValidationException("Custom mapper URL is not allowed: " + e.getMessage());
}
}
}
}

12
msa/js-executor/yarn.lock

@ -1036,9 +1036,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
@ -1549,9 +1549,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"
tar@>=7.5.8, tar@^7.4.3:
version "7.5.9"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8"
integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==
version "7.5.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"

14
msa/web-ui/config/custom-environment-variables.yml

@ -25,6 +25,20 @@ thingsboard:
host: "TB_HOST"
# ThingsBoard node port
port: "TB_PORT"
security:
headers:
x-content-type-options:
enabled: "SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED"
referrer-policy:
enabled: "SECURITY_HEADERS_REFERRER_POLICY_ENABLED"
value: "SECURITY_HEADERS_REFERRER_POLICY_VALUE"
x-frame-options:
enabled: "SECURITY_HEADERS_X_FRAME_OPTIONS_ENABLED"
value: "SECURITY_HEADERS_X_FRAME_OPTIONS_VALUE"
content-security-policy:
enabled: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_ENABLED"
value: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_VALUE"
report-only: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_REPORT_ONLY"
logger:
level: "LOGGER_LEVEL"
path: "LOG_FOLDER"

14
msa/web-ui/config/default.yml

@ -25,6 +25,20 @@ thingsboard:
host: "localhost"
# ThingsBoard node port
port: "8080"
security:
headers:
x-content-type-options:
enabled: true
referrer-policy:
enabled: true
value: "strict-origin-when-cross-origin"
x-frame-options:
enabled: false
value: "SAMEORIGIN"
content-security-policy:
enabled: false
value: ""
report-only: false
logger:
level: "info"
path: "logs"

30
msa/web-ui/server.ts

@ -60,6 +60,36 @@ let connections: Socket[] = [];
const app = express();
server = http.createServer(app);
// Build security headers map once at startup.
// node-config passes env var overrides as strings, so enabled can be boolean or string.
const isEnabled = (val: any) => val === true || val === 'true';
const securityHeaders: Record<string, string> = {};
const hc: any = config.get('security.headers');
if (isEnabled(hc['x-content-type-options']?.enabled)) {
securityHeaders['X-Content-Type-Options'] = 'nosniff';
}
if (isEnabled(hc['referrer-policy']?.enabled)) {
securityHeaders['Referrer-Policy'] = hc['referrer-policy']?.value || 'strict-origin-when-cross-origin';
}
if (isEnabled(hc['x-frame-options']?.enabled)) {
securityHeaders['X-Frame-Options'] = hc['x-frame-options']?.value || 'SAMEORIGIN';
}
if (isEnabled(hc['content-security-policy']?.enabled) && hc['content-security-policy']?.value) {
const csp = hc['content-security-policy'];
const name = isEnabled(csp['report-only'])
? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
securityHeaders[name] = csp.value;
}
logger.info('Security headers: %s', JSON.stringify(securityHeaders));
// Apply security headers to all responses
app.use((_req, res, next) => {
for (const [name, value] of Object.entries(securityHeaders)) {
res.setHeader(name, value);
}
next();
});
let apiProxy: httpProxy;
if (useApiProxy) {
apiProxy = httpProxy.createProxyServer({

12
msa/web-ui/yarn.lock

@ -1098,9 +1098,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
@ -1631,9 +1631,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"
tar@>=7.5.8, tar@^7.4.3:
version "7.5.9"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8"
integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==
version "7.5.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"

17
pom.xml

@ -534,6 +534,8 @@
<arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
<arg>-PpkgCopyInstallScripts=${pkg.copyInstallScripts}</arg>
<arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg>
<arg>--project-cache-dir</arg>
<arg>${project.build.directory}/.gradle</arg>
<arg>--warning-mode</arg>
<arg>all</arg>
</args>
@ -891,6 +893,21 @@
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<inherited>false</inherited>
<configuration>
<filesets>
<fileset>
<directory>${main.dir}/packaging/java/.gradle</directory>
</fileset>
<fileset>
<directory>${main.dir}/packaging/js/.gradle</directory>
</fileset>
</filesets>
</configuration>
</plugin>
</plugins>
</build>

174
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java

@ -0,0 +1,174 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.rest;
import io.netty.resolver.AddressResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.DefaultAddressResolverGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import org.thingsboard.common.util.SsrfProtectionValidator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Custom Netty {@link AddressResolverGroup} that validates every resolved IP address
* against the SSRF block-list at connection time. This eliminates the DNS rebinding
* TOCTOU gap where a hostname resolves to a safe IP during validation but to a
* private/metadata IP when the actual connection is made.
* <p>
* Only wired into {@link TbHttpClient} when SSRF protection is enabled.
*/
public final class SsrfSafeAddressResolverGroup extends AddressResolverGroup<InetSocketAddress> {
public static final SsrfSafeAddressResolverGroup INSTANCE = new SsrfSafeAddressResolverGroup();
private SsrfSafeAddressResolverGroup() {
}
@Override
protected AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
AddressResolver<InetSocketAddress> delegate = DefaultAddressResolverGroup.INSTANCE.getResolver(executor);
return new SsrfValidatingResolver(executor, delegate);
}
private static final class SsrfValidatingResolver implements AddressResolver<InetSocketAddress> {
private final EventExecutor executor;
private final AddressResolver<InetSocketAddress> delegate;
SsrfValidatingResolver(EventExecutor executor, AddressResolver<InetSocketAddress> delegate) {
this.executor = executor;
this.delegate = delegate;
}
@Override
public boolean isSupported(SocketAddress address) {
return delegate.isSupported(address);
}
@Override
public boolean isResolved(SocketAddress address) {
return delegate.isResolved(address);
}
@Override
public Future<InetSocketAddress> resolve(SocketAddress address) {
return resolve(address, executor.newPromise());
}
@Override
public Future<InetSocketAddress> resolve(SocketAddress address, Promise<InetSocketAddress> promise) {
delegate.resolve(address).addListener((Future<InetSocketAddress> future) -> {
try {
if (!future.isSuccess()) {
promise.tryFailure(future.cause());
return;
}
InetSocketAddress resolved = future.getNow();
if (isOriginalHostAllowed(address)) {
promise.trySuccess(resolved);
} else if (isBlocked(resolved)) {
promise.tryFailure(new RuntimeException(
"URI is invalid: host '" + getHostString(address) + "' is not allowed"));
} else {
promise.trySuccess(resolved);
}
} catch (Exception e) {
promise.tryFailure(e);
}
});
return promise;
}
@Override
public Future<List<InetSocketAddress>> resolveAll(SocketAddress address) {
return resolveAll(address, executor.newPromise());
}
@Override
public Future<List<InetSocketAddress>> resolveAll(SocketAddress address, Promise<List<InetSocketAddress>> promise) {
delegate.resolveAll(address).addListener((Future<List<InetSocketAddress>> future) -> {
try {
if (!future.isSuccess()) {
promise.tryFailure(future.cause());
return;
}
List<InetSocketAddress> resolved = future.getNow();
if (isOriginalHostAllowed(address)) {
promise.trySuccess(resolved);
return;
}
Set<InetSocketAddress> blocked = null;
for (InetSocketAddress addr : resolved) {
if (isBlocked(addr)) {
if (blocked == null) {
blocked = new HashSet<>(2);
}
blocked.add(addr);
}
}
if (blocked == null) {
promise.trySuccess(resolved);
} else if (blocked.size() == resolved.size()) {
promise.tryFailure(new RuntimeException(
"URI is invalid: host '" + getHostString(address) + "' is not allowed"));
} else {
List<InetSocketAddress> safe = new ArrayList<>(resolved.size() - blocked.size());
for (InetSocketAddress addr : resolved) {
if (!blocked.contains(addr)) {
safe.add(addr);
}
}
promise.trySuccess(safe);
}
} catch (Exception e) {
promise.tryFailure(e);
}
});
return promise;
}
@Override
public void close() {
delegate.close();
}
private static boolean isBlocked(InetSocketAddress socketAddress) {
InetAddress addr = socketAddress.getAddress();
return addr != null && SsrfProtectionValidator.isBlockedAddress(addr);
}
private static boolean isOriginalHostAllowed(SocketAddress address) {
if (address instanceof InetSocketAddress isa) {
String host = isa.getHostString();
return host != null && SsrfProtectionValidator.isHostnameAllowed(host);
}
return false;
}
private static String getHostString(SocketAddress address) {
return address instanceof InetSocketAddress isa ? isa.getHostString() : address.toString();
}
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -103,6 +103,7 @@ public class TbHttpClient {
.build();
HttpClient httpClient = HttpClient.create(connectionProvider)
.followRedirect(false)
.runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared))
.doOnConnected(c ->
c.addHandlerLast(new ReadTimeoutHandler(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)));
@ -138,6 +139,10 @@ public class TbHttpClient {
httpClient = httpClient.secure(t -> t.sslContext(sslContext));
}
if (SsrfProtectionValidator.isEnabled()) {
httpClient = httpClient.resolver(SsrfSafeAddressResolverGroup.INSTANCE);
}
validateMaxInMemoryBufferSize(config);
this.webClient = WebClient.builder()

161
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java

@ -0,0 +1,161 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.rest;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.resolver.AddressResolver;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Promise;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.thingsboard.common.util.SsrfProtectionValidator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ResourceLock("SsrfSafeAddressResolverGroupTest")
class SsrfSafeAddressResolverGroupTest {
private static NioEventLoopGroup eventLoopGroup;
@BeforeAll
static void setUp() {
eventLoopGroup = new NioEventLoopGroup(1);
}
@AfterAll
static void tearDown() {
eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
SsrfProtectionValidator.setEnabled(false);
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
@BeforeEach
void enableSsrf() {
SsrfProtectionValidator.setEnabled(true);
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
@AfterEach
void resetState() {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
SsrfProtectionValidator.setEnabled(false);
}
@Test
void isBlockedAddressWorksForLoopback() throws Exception {
assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("127.0.0.1"))).isTrue();
assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("192.168.1.1"))).isTrue();
assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("8.8.8.8"))).isFalse();
}
@Test
void resolvePublicIpSucceeds() throws Exception {
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("8.8.8.8", 80), promise));
InetSocketAddress result = promise.get(10, TimeUnit.SECONDS);
assertThat(result.getAddress()).isNotNull();
assertThat(result.getAddress().getHostAddress()).isEqualTo("8.8.8.8");
}
@Test
void resolveLoopbackFailsWhenSsrfEnabled() throws Exception {
assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("127.0.0.1", 80), promise));
assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("is not allowed");
}
@Test
void resolvePrivateIpFailsWhenSsrfEnabled() throws Exception {
assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("192.168.1.1", 80), promise));
assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("is not allowed");
}
@Test
void resolveAllowedPrivateIpSucceeds() throws Exception {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("192.168.1.1", 80), promise));
InetSocketAddress result = promise.get(10, TimeUnit.SECONDS);
assertThat(result.getAddress().getHostAddress()).isEqualTo("192.168.1.1");
}
@Test
void resolveAllPublicIpSucceeds() throws Exception {
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<List<InetSocketAddress>> promise = executor.newPromise();
executor.submit(() -> resolver.resolveAll(InetSocketAddress.createUnresolved("8.8.8.8", 80), promise));
List<InetSocketAddress> results = promise.get(10, TimeUnit.SECONDS);
assertThat(results).isNotEmpty();
assertThat(results.get(0).getAddress().getHostAddress()).isEqualTo("8.8.8.8");
}
@Test
void resolveAllPrivateIpFailsWhenSsrfEnabled() {
assertThatThrownBy(() -> {
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<List<InetSocketAddress>> promise = executor.newPromise();
executor.submit(() -> resolver.resolveAll(InetSocketAddress.createUnresolved("127.0.0.1", 80), promise));
promise.get(10, TimeUnit.SECONDS);
}).isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("is not allowed");
}
}

32
ui-ngx/package.json

@ -13,16 +13,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "20.3.17",
"@angular/animations": "20.3.18",
"@angular/cdk": "20.2.14",
"@angular/common": "20.3.17",
"@angular/compiler": "20.3.17",
"@angular/core": "20.3.17",
"@angular/forms": "20.3.17",
"@angular/common": "20.3.18",
"@angular/compiler": "20.3.18",
"@angular/core": "20.3.18",
"@angular/forms": "20.3.18",
"@angular/material": "20.2.14",
"@angular/platform-browser": "20.3.17",
"@angular/platform-browser-dynamic": "20.3.17",
"@angular/router": "20.3.17",
"@angular/platform-browser": "20.3.18",
"@angular/platform-browser-dynamic": "20.3.18",
"@angular/router": "20.3.18",
"@auth0/angular-jwt": "^5.2.0",
"@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "20.0.2",
@ -94,13 +94,13 @@
},
"devDependencies": {
"@angular-builders/custom-esbuild": "20.0.0",
"@angular-devkit/build-angular": "20.3.18",
"@angular-devkit/core": "20.3.18",
"@angular-devkit/schematics": "20.3.18",
"@angular/build": "20.3.18",
"@angular/cli": "20.3.18",
"@angular/compiler-cli": "20.3.17",
"@angular/language-service": "20.3.17",
"@angular-devkit/build-angular": "20.3.20",
"@angular-devkit/core": "20.3.20",
"@angular-devkit/schematics": "20.3.20",
"@angular/build": "20.3.20",
"@angular/cli": "20.3.20",
"@angular/compiler-cli": "20.3.18",
"@angular/language-service": "20.3.18",
"@types/ace-diff": "^2.1.4",
"@types/canvas-gauges": "^2.1.8",
"@types/flot": "^0.0.36",
@ -121,7 +121,7 @@
"angular-eslint": "~20.7.0",
"autoprefixer": "^10.4.23",
"directory-tree": "^3.5.2",
"eslint": "9.39.3",
"eslint": "9.39.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsdoc": "^62.4.1",
"eslint-plugin-prefer-arrow": "^1.2.3",

0
ui-ngx/patches/@angular+build+20.3.18.patch → ui-ngx/patches/@angular+build+20.3.20.patch

4
ui-ngx/patches/@angular+core+20.3.17.patch → ui-ngx/patches/@angular+core+20.3.18.patch

@ -1,8 +1,8 @@
diff --git a/node_modules/@angular/core/fesm2022/debug_node.mjs b/node_modules/@angular/core/fesm2022/debug_node.mjs
index d9be60f..24891ce 100755
index 35c61af..d89462b 100755
--- a/node_modules/@angular/core/fesm2022/debug_node.mjs
+++ b/node_modules/@angular/core/fesm2022/debug_node.mjs
@@ -9421,13 +9421,13 @@ function findDirectiveDefMatches(tView, tNode) {
@@ -9428,13 +9428,13 @@ function findDirectiveDefMatches(tView, tNode) {
if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) {
matches ??= [];
if (isComponentDef(def)) {

49
ui-ngx/src/app/core/ws/websocket.service.ts

@ -29,9 +29,11 @@ import {
WebsocketDataMsg
} from '@shared/models/telemetry/telemetry.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { NotificationType } from '@core/notification/notification.models';
import Timeout = NodeJS.Timeout;
const RECONNECT_INTERVAL = 2000;
const MAX_RECONNECT_INTERVAL = 60000;
const WS_IDLE_TIMEOUT = 90000;
const MAX_PUBLISH_COMMANDS = 10;
@ -57,6 +59,16 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
errorName = 'WebSocket Error';
// Exponential backoff: tracks the number of consecutive failed reconnect attempts.
// Reset only after a productive connection (i.e. at least one message received).
// This prevents the open→immediately-closed cycle from resetting the counter.
private reconnectAttempts = 0;
// Stores the last close-event error code shown to the user during a reconnect cycle.
// Only suppresses duplicate notifications for the same error code; a new error code is still shown.
// Cleared after receiving a successful message.
private lastShownCloseCode: number | null = null;
protected constructor(protected store: Store<AppState>,
protected authService: AuthService,
protected ngZone: NgZone,
@ -126,6 +138,8 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
this.subscribersCount = 0;
this.cmdWrapper.clear();
if (close) {
this.reconnectAttempts = 0;
this.lastShownCloseCode = null;
this.closeSocket();
}
}
@ -221,6 +235,10 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
this.processOnMessage(message as WebsocketDataMsg);
}
this.checkToClose();
if (this.reconnectAttempts) {
this.reconnectAttempts = 0;
this.lastShownCloseCode = null;
}
}
private onError(errorEvent) {
@ -231,8 +249,13 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
}
private onClose(closeEvent: CloseEvent) {
// Show error notification only when the error code changes to prevent notification spam,
// while still surfacing new, potentially actionable errors during a reconnect cycle.
// lastShownCloseCode is cleared only after a productive connection (onMessage).
if (closeEvent && closeEvent.code > 1001 && closeEvent.code !== 1006
&& closeEvent.code !== 1011 && closeEvent.code !== 1012 && closeEvent.code !== 4500) {
&& closeEvent.code !== 1011 && closeEvent.code !== 1012 && closeEvent.code !== 4500
&& this.lastShownCloseCode !== closeEvent.code) {
this.lastShownCloseCode = closeEvent.code;
this.showWsError(closeEvent.code, closeEvent.reason);
}
this.isOpening = false;
@ -251,18 +274,28 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
this.reconnectTimer = setTimeout(() => this.tryOpenSocket(), RECONNECT_INTERVAL);
const delay = Math.min(RECONNECT_INTERVAL * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_INTERVAL);
this.reconnectAttempts = Math.min(this.reconnectAttempts + 1, 10);
this.reconnectTimer = setTimeout(() => this.tryOpenSocket(), delay);
}
}
private showWsError(errorCode: number, errorMsg: string) {
let message = errorMsg;
if (!message) {
message += `${this.errorName}: error code - ${errorCode}.`;
let notificationType: NotificationType = 'error';
if (errorCode === 1008 || (errorMsg && errorMsg.includes('limit reached'))) {
message = 'Too many active sessions. Please close unused browser tabs or sign out from other devices';
notificationType = 'warn';
} else if (errorCode === 1009) {
message = 'Too much data to display. Please refresh the page or narrow your request.';
notificationType = 'warn';
} else if (!message) {
message = `${this.errorName}: error code - ${errorCode}.`;
}
this.store.dispatch(new ActionNotificationShow(
{
message, type: 'error'
}));
this.store.dispatch(new ActionNotificationShow({
message, type: notificationType
}));
}
}

372
ui-ngx/yarn.lock

@ -160,24 +160,24 @@
"@angular-devkit/core" "^20.0.0"
"@angular/build" "^20.0.0"
"@angular-devkit/architect@0.2003.18", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0":
version "0.2003.18"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.18.tgz#60a1dcfe1d2401787e25e716059c81a9c8e6a08b"
integrity sha512-pPEDby3wQb40YSpH+UrjodJ78Z7q0Qvy3DTkS7mP2EIM4r0WVz8OlxLGS2uAc6tXSbIZe0bPp0B56P6uet3tUw==
"@angular-devkit/architect@0.2003.20", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0":
version "0.2003.20"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.20.tgz#152edd3078be72b72c814fc625222e5ff5c90916"
integrity sha512-1g7q37Aq4dvDdQDW0PtWXfiX5hBV78K74QUtFkAXGIXU3DkguwOQaqHILCnIRAVr0wFlWDckWVuO5OT6Cl9HeQ==
dependencies:
"@angular-devkit/core" "20.3.18"
"@angular-devkit/core" "20.3.20"
rxjs "7.8.2"
"@angular-devkit/build-angular@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.18.tgz#41acc206eff8a20452520106949b11d231764f4f"
integrity sha512-ERjoEHnWDi9FFf7HBvJuWoDVHHEBvUL43vPp7vUAf3+0u/qOzXmuxFccdzT72BM1wU3y70MAXB76TUkr/KrxBA==
"@angular-devkit/build-angular@20.3.20":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.20.tgz#452fdb36b0f931086107ee0c3af34c7882017037"
integrity sha512-ihBD8zsRoMsuwS3KkwqwwlJGw5jcLerG8zy1kdSEsdJwIYjITgMdV+iK02uSWptLTDd3ILOdgcoZzipcEwJX4w==
dependencies:
"@ampproject/remapping" "2.3.0"
"@angular-devkit/architect" "0.2003.18"
"@angular-devkit/build-webpack" "0.2003.18"
"@angular-devkit/core" "20.3.18"
"@angular/build" "20.3.18"
"@angular-devkit/architect" "0.2003.20"
"@angular-devkit/build-webpack" "0.2003.20"
"@angular-devkit/core" "20.3.20"
"@angular/build" "20.3.20"
"@babel/core" "7.28.3"
"@babel/generator" "7.28.3"
"@babel/helper-annotate-as-pure" "7.27.3"
@ -188,12 +188,12 @@
"@babel/preset-env" "7.28.3"
"@babel/runtime" "7.28.3"
"@discoveryjs/json-ext" "0.6.3"
"@ngtools/webpack" "20.3.18"
"@ngtools/webpack" "20.3.20"
ansi-colors "4.1.3"
autoprefixer "10.4.21"
babel-loader "10.0.0"
browserslist "^4.21.5"
copy-webpack-plugin "13.0.1"
copy-webpack-plugin "14.0.0"
css-loader "7.1.2"
esbuild-wasm "0.25.9"
fast-glob "3.3.3"
@ -230,15 +230,15 @@
optionalDependencies:
esbuild "0.25.9"
"@angular-devkit/build-webpack@0.2003.18":
version "0.2003.18"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.18.tgz#990a4670363346be3eff60e99163aa03b9257841"
integrity sha512-E70aiRrmnzk+nMQnEpg7KemqhjyI2X53zCC+1SGAVIISmtKyHQNoJDXa48im+bhrcjHpY2LOo0bFgb/vcWHc8A==
"@angular-devkit/build-webpack@0.2003.20":
version "0.2003.20"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.20.tgz#e6fc4b16b448f2c45b46611ed602a9c34be7d67d"
integrity sha512-WLeYLFxRnEYwCrPUx8qciWs+PrJKDp71ZsIRqeFCcpB8qFQOgR/CjPZYxZFqAnuTJtM4MqCtczvmn57ulKTPcA==
dependencies:
"@angular-devkit/architect" "0.2003.18"
"@angular-devkit/architect" "0.2003.20"
rxjs "7.8.2"
"@angular-devkit/core@20.3.18", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0":
"@angular-devkit/core@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.18.tgz#a079775ba6a31583a0d57813b374a6c8c997f252"
integrity sha512-zGWMjMqE8qXYr8baYCs43k9HlKz9J4Gh3Yx+7XE0uS0Y1LXzzALevSoUw7GIPdSvOriQJAEgtWE6QKssqSGltQ==
@ -250,12 +250,24 @@
rxjs "7.8.2"
source-map "0.7.6"
"@angular-devkit/schematics@20.3.18", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.18.tgz#da5f9a84b2df9f13049974b6cd582475a3355d28"
integrity sha512-GRMEGl3YTL/qhQhaxYXLbSQxUTPTYMQ65IlxLQRq5+UKPomN9KVxxVdADXqs7Ss1uQcetr+jc+taVgxOqsAoxg==
"@angular-devkit/core@20.3.20", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.20.tgz#62278795afb05ff1c2f442387e5c3e996ac304bf"
integrity sha512-Iobw7He3yJVR2aQ6JN9Kq/2ldD8+uHzJZwd41SQ91A+TzPrBRSV0t80WHHrANZ7xnAjtHDc7zSSGp/i7DzUc9g==
dependencies:
ajv "8.18.0"
ajv-formats "3.0.1"
jsonc-parser "3.3.1"
picomatch "4.0.3"
rxjs "7.8.2"
source-map "0.7.6"
"@angular-devkit/schematics@20.3.20", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.20.tgz#d7d2cc74134823305b9451a1920130daecdf8c61"
integrity sha512-+NNHQhQHcgQWZopStZ6os30YuP99lRzNS4wOnkJmoROy40SZct8lPnl2QW50a9Vc0AtaHx1a1NUZ+ohbf6fXqw==
dependencies:
"@angular-devkit/core" "20.3.18"
"@angular-devkit/core" "20.3.20"
jsonc-parser "3.3.1"
magic-string "0.30.17"
ora "8.2.0"
@ -321,20 +333,20 @@
dependencies:
"@angular-eslint/bundled-angular-compiler" "20.7.0"
"@angular/animations@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-20.3.17.tgz#a50991c88a148b7345d679a9961210942d5d6481"
integrity sha512-KvdgFjCTkOD3WVt4gzmJOoX914eey/Efu2Pb/KUM0Bqp1ZoXiFpI48GCd1b6Ks8JlDBeAfgjtpdSUB2aLnMRZQ==
"@angular/animations@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-20.3.18.tgz#e675e045839d559b4917053eb0f74313bf3235ef"
integrity sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==
dependencies:
tslib "^2.3.0"
"@angular/build@20.3.18", "@angular/build@^20.0.0":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.18.tgz#c9bdf9b94c62b9225509f04fcedfde822847c8bf"
integrity sha512-t+Bg0uxnyrbm5ADa8Ka5rz4bSdf8ScCnY8Hua3bLnIPITzeuuunV7a14zMSOcPL6eLu0760CvszHsGX1k6aN7A==
"@angular/build@20.3.20", "@angular/build@^20.0.0":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.20.tgz#8692af778824d43cd4e363330494144a70fa1950"
integrity sha512-+uWqGU+Qyso2uJKL1xNjAm+E3m3ncv5InMUG5BC/UhFnXTsL1o7oySGc6hHa+8rQunuifUhdy20HxZjU6QlqTw==
dependencies:
"@ampproject/remapping" "2.3.0"
"@angular-devkit/architect" "0.2003.18"
"@angular-devkit/architect" "0.2003.20"
"@babel/core" "7.28.3"
"@babel/helper-annotate-as-pure" "7.27.3"
"@babel/helper-split-export-declaration" "7.24.7"
@ -370,18 +382,18 @@
parse5 "^8.0.0"
tslib "^2.3.0"
"@angular/cli@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.18.tgz#3f635c64818c64df8d9358aa6b3680fad48c36cb"
integrity sha512-I0kanxt3vzedZmLY4FLoxgo3yGG1mWoiGLlzwEslJdLJj5X1zd422WPtTygZgEHFHcGxR9qxdQ+PsPdMRwykQA==
"@angular/cli@20.3.20":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.20.tgz#adbffbbcaf5ffda133e2f86dcb83dbfb86671ca4"
integrity sha512-wHMo0BWhoBMXcsZ1U+yJ0eOH2B5NgCWW7PTrJ1P/QbocbCSh7eqw8wKnEthIKyhrEWtAo67Jw7i8CgbeIZgmMw==
dependencies:
"@angular-devkit/architect" "0.2003.18"
"@angular-devkit/core" "20.3.18"
"@angular-devkit/schematics" "20.3.18"
"@angular-devkit/architect" "0.2003.20"
"@angular-devkit/core" "20.3.20"
"@angular-devkit/schematics" "20.3.20"
"@inquirer/prompts" "7.8.2"
"@listr2/prompt-adapter-inquirer" "3.0.1"
"@modelcontextprotocol/sdk" "1.26.0"
"@schematics/angular" "20.3.18"
"@schematics/angular" "20.3.20"
"@yarnpkg/lockfile" "1.1.0"
algoliasearch "5.35.0"
ini "5.0.0"
@ -394,17 +406,17 @@
yargs "18.0.0"
zod "4.1.13"
"@angular/common@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-20.3.17.tgz#891d08610683c2b22edd3e64684647a911458cee"
integrity sha512-Dqd8f8o9MehszTZIB7o7jrERlwLOSK64gNngK14DCQazz5lpIhAF6hBjx7zjHpa7L9eAYPK1TaxQUXypjzj18Q==
"@angular/common@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-20.3.18.tgz#311dc0658be69f1368db2eeea92ea1b940329975"
integrity sha512-M62oQbSTRmnGavIVCwimoadg/PDWadgNhactMm9fgH0eM9rx+iWBAYJk4VufO0bwOhysFpRZpJgXlFjOifz/Jw==
dependencies:
tslib "^2.3.0"
"@angular/compiler-cli@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-20.3.17.tgz#e1ab0799090e9e5b8d656f432ab102db080d9b34"
integrity sha512-w5pmO1pXO9tUMgUMWstpDmAWh5s1lJWo+2GI/ByaUEgBZkXd2S92sWoDL+bhy+JSvFzdLGdua6BncHBOX7hEjA==
"@angular/compiler-cli@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-20.3.18.tgz#0f4726d1624c9def0f60c65f60e61a226676459c"
integrity sha512-zsoEgLgnblmRbi47YwMghKirJ8IBKJ3+I8TxLBRIBrhx+KHFp+6oeDeLyu9H+djdyk88zexVd09wzR/YK73F0g==
dependencies:
"@babel/core" "7.28.3"
"@jridgewell/sourcemap-codec" "^1.4.14"
@ -415,31 +427,31 @@
tslib "^2.3.0"
yargs "^18.0.0"
"@angular/compiler@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-20.3.17.tgz#ae64caadfc80ebc08d145723a7fa107d710ee7a8"
integrity sha512-cj3x6aFk9xOOxX+qEdeN8T5YbnBNWJ4UMHB/LQoDr7/xCJJGa40IhcOAuJeuF2kGqTwx6MCXnvjO8XOQfHhe9g==
"@angular/compiler@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-20.3.18.tgz#5370dc1a24d55623828a2b0875c776c8da13fdc6"
integrity sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA==
dependencies:
tslib "^2.3.0"
"@angular/core@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-20.3.17.tgz#e396f0f2a6f47a85524e8d313082f65e35001059"
integrity sha512-YlQqxMeHI9XJw7I7oM3hYFQd4lQbK37IdlD9ztROIw5FjX6i6lmLU7+X1MQGSRi2r+X9l3IZtl33hRTNvkoUBw==
"@angular/core@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-20.3.18.tgz#af91805841184cd87e862134a806bba019d170ea"
integrity sha512-B+NQQngd/aDbcfW0zGLis3wTLDeHTeTYMl/mGKQH+HwdPaRCKI1wEtaXaOYVJXkP2FeThocPevB8gLwNlPQUUw==
dependencies:
tslib "^2.3.0"
"@angular/forms@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-20.3.17.tgz#94735ced545516ae52ccc59e1593eeaa6bae8edb"
integrity sha512-iGS6NwzcyJzinbPMapsQtcN0ZJ62vr6hcul+FNa40CaK2ePC04S+C5n+DIphzwnwsFHDBIWuTQRfk/lNYdN1JA==
"@angular/forms@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-20.3.18.tgz#ebd249a381dd3f6e5af3f760d7c3a8a6a76531ac"
integrity sha512-x6/99LfxolyZIFUL3Wr0OrtuXHEDwEz/rwx+WzE7NL+n35yO40t3kp0Sn5uMFwI94i91QZJmXHltMpZhrVLuYg==
dependencies:
tslib "^2.3.0"
"@angular/language-service@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-20.3.17.tgz#8dd38151eb53db0a96138d2711e3fcaeb2b2cfed"
integrity sha512-ccNK/+FDXHWeQwfRKghzk4JDEtoT1QQA6GWDTujn+/pgai6NjQ0NNKGbqgrC1zOzEQRUo4Nz6xN1UXQSYMxCnA==
"@angular/language-service@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-20.3.18.tgz#b0c8c9bcf085907765e3cb7fbf0e14e79259e3c0"
integrity sha512-V1ZBqeTtZYH9H8/G1qCw6gafsJmhMIMFjLX0Hv2KpTpmfK9nxIHPEVnshr3xT+qKYJIrMV/cU5YOzInEapLpuQ==
"@angular/material@20.2.14":
version "20.2.14"
@ -448,24 +460,24 @@
dependencies:
tslib "^2.3.0"
"@angular/platform-browser-dynamic@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.17.tgz#c3dc69593176ac3b8802eaa9c0d3ab86c642ee11"
integrity sha512-yTxFuGQ+z0J9khNIhfFZ+kkT7TOFb8kFZKyUz0DxHOmE0q/TEvNZoy3jXOs8xCBFf1+6BY0NqFNlPna+uw36FQ==
"@angular/platform-browser-dynamic@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.18.tgz#c4c633fcc4782e1a361a0904d2bc3f180bbf0f81"
integrity sha512-NyTobOGYVzGmPmtI+3lxMzxi0TbLq4SRNQ2ENEJAt6k2JnMmHBm483ppLRAM47nGlDdiraW0IX93EtYYNkiK3g==
dependencies:
tslib "^2.3.0"
"@angular/platform-browser@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-20.3.17.tgz#e156693a769284859fdbac76c95f6cc738bcd2f9"
integrity sha512-GA8pK+0F2/KGdYn5LMpLBrPTkQUwGjQE8Q+qsivOa150cK3OuD0po5PvYK58l+niGIVvm0wB1xGKTHTOiX/+4A==
"@angular/platform-browser@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-20.3.18.tgz#33b5d5cdddc4a0f60a9ddf886b941587a348d304"
integrity sha512-q6s5rEN1yYazpHYp+k4pboXRzMsRB9auzTRBEhyXSGYxqzrnn3qHN0DqgsLC9WAdyhCgnIEMFA8kRT+W277DqQ==
dependencies:
tslib "^2.3.0"
"@angular/router@20.3.17":
version "20.3.17"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-20.3.17.tgz#a9390ce0c3f2fbdbc02ffc1b224e375bd5e0e0d3"
integrity sha512-p0r0IOJhUcn8WHx4gkSlfwifkkYO5mSDtq4iM5OunZTlSaeSxLb1vTRg2VBgwdzpgAM+eZSMBTTVF/M3pdoELQ==
"@angular/router@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-20.3.18.tgz#276fe53975ceb858f4e76405f8219fbe5cb20204"
integrity sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng==
dependencies:
tslib "^2.3.0"
@ -1531,14 +1543,14 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
"@eslint/config-array@^0.21.1":
version "0.21.1"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713"
integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==
"@eslint/config-array@^0.21.2":
version "0.21.2"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.2.tgz#f29e22057ad5316cf23836cee9a34c81fffcb7e6"
integrity sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==
dependencies:
"@eslint/object-schema" "^2.1.7"
debug "^4.3.1"
minimatch "^3.1.2"
minimatch "^3.1.5"
"@eslint/config-helpers@^0.4.2":
version "0.4.2"
@ -1554,25 +1566,25 @@
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.1":
version "3.3.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac"
integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
"@eslint/eslintrc@^3.3.5":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60"
integrity sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==
dependencies:
ajv "^6.12.4"
ajv "^6.14.0"
debug "^4.3.2"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.1"
minimatch "^3.1.2"
minimatch "^3.1.5"
strip-json-comments "^3.1.1"
"@eslint/js@9.39.3":
version "9.39.3"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.3.tgz#c6168736c7e0c43ead49654ed06a4bcb3833363d"
integrity sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==
"@eslint/js@9.39.4":
version "9.39.4"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.4.tgz#a3f83bfc6fd9bf33a853dfacd0b49b398eb596c1"
integrity sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==
"@eslint/object-schema@^2.1.7":
version "2.1.7"
@ -1612,9 +1624,9 @@
polyclip-ts "^0.16.5"
"@hono/node-server@^1.19.9":
version "1.19.9"
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.9.tgz#8f37119b1acf283fd3f6035f3d1356fdb97a09ac"
integrity sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==
version "1.19.11"
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.11.tgz#dc419f0826dd2504e9fc86ad289d5636a0444e2f"
integrity sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==
"@humanfs/core@^0.19.1":
version "0.19.1"
@ -2397,10 +2409,10 @@
dependencies:
tslib "^2.0.0"
"@ngtools/webpack@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.18.tgz#b32c7e2b9681bf1813077efc99b781ca8cc37999"
integrity sha512-dFH/K6byV9oWbLIDoI/RTgYkbIqaofNr7PHVkH8MhMi/rSoKEgf2Un/xjaD7zCsODHou7HE/jfiXgC+6Adzveg==
"@ngtools/webpack@20.3.20":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.20.tgz#4d45b14ee374349352b90b2db6049b9901f80b9d"
integrity sha512-5azJ9+W/aMFR4C38ShoWib6xW5ou5Q5yeup+6skpvrud5mAAj3e36S5diYYXqbzVfo3DM+sWueDRNP3DV7IHhg==
"@ngx-translate/core@^17.0.0":
version "17.0.0"
@ -2751,13 +2763,13 @@
resolved "https://registry.yarnpkg.com/@sanity/diff-match-patch/-/diff-match-patch-3.2.0.tgz#7ce587273f7372a146308cb1075ba26177d42cdb"
integrity sha512-4hPADs0qUThFZkBK/crnfKKHg71qkRowfktBljH2UIxGHHTxIzt8g8fBiXItyCjxkuNy+zpYOdRMifQNv8+Yww==
"@schematics/angular@20.3.18":
version "20.3.18"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.18.tgz#5b435f15837ac8b6faaf34fa63ab8589d67b3737"
integrity sha512-JZdvBNrWODBTLrmtUF6+UD26z5cENpV0X9liR1jPDT1O7taQqwRePSuCQcjRo1qXCjlNfBW7pGGVxVCRKK8EXw==
"@schematics/angular@20.3.20":
version "20.3.20"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.20.tgz#22b0e87bab0aeca0ddbaef7014eac5033f813972"
integrity sha512-LyoHwenNi8lZZO5zafAjcN8DcaOdjFrkbZdrCkvdpmOFz5wy8ZGchY6XSZEDD6kdHvR8oU7WWnGTMgCfExBKCg==
dependencies:
"@angular-devkit/core" "20.3.18"
"@angular-devkit/schematics" "20.3.18"
"@angular-devkit/core" "20.3.20"
"@angular-devkit/schematics" "20.3.20"
jsonc-parser "3.3.1"
"@sigstore/bundle@^4.0.0":
@ -3696,7 +3708,7 @@ adjust-sourcemap-loader@^4.0.0:
loader-utils "^2.0.0"
regex-parser "^2.2.11"
agent-base@^7.1.0, agent-base@^7.1.1, agent-base@^7.1.2:
agent-base@^7.1.0, agent-base@^7.1.2:
version "7.1.4"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
@ -3732,10 +3744,10 @@ ajv@8.18.0, ajv@^8.0.0, ajv@^8.17.1, ajv@^8.9.0:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
ajv@^6.14.0:
version "6.14.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
@ -4605,15 +4617,15 @@ copy-anything@^2.0.1:
dependencies:
is-what "^3.14.1"
copy-webpack-plugin@13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz#fba18c22bcab3633524e1b652580ff4489eddc0d"
integrity sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==
copy-webpack-plugin@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz#cd253b60e8e55bb41019dfe3ef2979ba705592c7"
integrity sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==
dependencies:
glob-parent "^6.0.1"
normalize-path "^3.0.0"
schema-utils "^4.2.0"
serialize-javascript "^6.0.2"
serialize-javascript "^7.0.3"
tinyglobby "^0.2.12"
core-js-compat@^3.43.0:
@ -5685,24 +5697,24 @@ eslint-visitor-keys@^5.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9"
integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==
eslint@9.39.3:
version "9.39.3"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.3.tgz#08d63df1533d7743c0907b32a79a7e134e63ee2f"
integrity sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==
eslint@9.39.4:
version "9.39.4"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.4.tgz#855da1b2e2ad66dc5991195f35e262bcec8117b5"
integrity sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.21.1"
"@eslint/config-array" "^0.21.2"
"@eslint/config-helpers" "^0.4.2"
"@eslint/core" "^0.17.0"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.39.3"
"@eslint/eslintrc" "^3.3.5"
"@eslint/js" "9.39.4"
"@eslint/plugin-kit" "^0.4.1"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.4.2"
"@types/estree" "^1.0.6"
ajv "^6.12.4"
ajv "^6.14.0"
chalk "^4.0.0"
cross-spawn "^7.0.6"
debug "^4.3.2"
@ -5721,7 +5733,7 @@ eslint@9.39.3:
is-glob "^4.0.0"
json-stable-stringify-without-jsonify "^1.0.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
minimatch "^3.1.5"
natural-compare "^1.4.0"
optionator "^0.9.3"
@ -5815,11 +5827,11 @@ exponential-backoff@^3.1.1:
integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==
express-rate-limit@^8.2.1:
version "8.2.1"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.2.1.tgz#ec75fdfe280ecddd762b8da8784c61bae47d7f7f"
integrity sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==
version "8.3.1"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.3.1.tgz#0aaba098eadd40f6737f30a98e6b16fa1a29edfb"
integrity sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==
dependencies:
ip-address "10.0.1"
ip-address "10.1.0"
express@^4.21.2:
version "4.22.1"
@ -6033,9 +6045,9 @@ flat@^5.0.2:
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
flatted@^3.2.9:
version "3.3.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27"
integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==
version "3.4.1"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.1.tgz#84ccd9579e76e9cc0d246c11d8be0beb019143e6"
integrity sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==
"flot.curvedlines@https://github.com/MichaelZinsmaier/CurvedLines.git#master":
version "1.1.1"
@ -6383,9 +6395,9 @@ hasown@^2.0.2:
function-bind "^1.1.2"
hono@^4.11.4:
version "4.12.2"
resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.2.tgz#05c311c271b06685a0f229c484e3a2637d7d5f2a"
integrity sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==
version "4.12.8"
resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.8.tgz#5f3a9c0d5339ff460b2c652a4c64dd79059930ad"
integrity sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==
hosted-git-info@^9.0.0:
version "9.0.2"
@ -6575,9 +6587,9 @@ immediate@~3.0.5:
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immutable@^5.0.2:
version "5.1.4"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.4.tgz#e3f8c1fe7b567d56cf26698f31918c241dae8c1f"
integrity sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==
version "5.1.5"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165"
integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==
import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.0"
@ -6641,18 +6653,10 @@ internmap@^1.0.0:
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
ip-address@10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed"
integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
ip-address@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==
dependencies:
jsbn "1.1.0"
sprintf-js "^1.1.3"
ip-address@10.1.0, ip-address@^10.0.1:
version "10.1.0"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
ipaddr.js@1.9.1:
version "1.9.1"
@ -7080,11 +7084,6 @@ js-yaml@^4.1.0, js-yaml@^4.1.1:
dependencies:
argparse "^2.0.1"
jsbn@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
jsdoc-type-pratt-parser@~7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.1.0.tgz#f2d63cbbc3d0d4eaea257eb5f847e8ebc5908dd5"
@ -7761,10 +7760,10 @@ minimatch@^10.0.3, minimatch@^10.1.1:
dependencies:
brace-expansion "^5.0.2"
minimatch@^3.1.2:
version "3.1.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.4.tgz#89d910ea3970a77ac8edfd30340ccd038b758079"
integrity sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==
minimatch@^3.1.2, minimatch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
@ -8865,13 +8864,6 @@ quickselect@^3.0.0:
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603"
integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
dependencies:
safe-buffer "^5.1.0"
range-parser@^1.2.1, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -9223,7 +9215,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -9379,12 +9371,10 @@ send@~0.19.0, send@~0.19.1:
range-parser "~1.2.1"
statuses "~2.0.2"
serialize-javascript@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
dependencies:
randombytes "^2.1.0"
serialize-javascript@^7.0.3:
version "7.0.4"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.4.tgz#c517735bd5b7631dd1fc191ee19cbb713ff8e05c"
integrity sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==
serve-index@^1.9.1:
version "1.9.1"
@ -9587,20 +9577,20 @@ sockjs@^0.3.24:
websocket-driver "^0.7.4"
socks-proxy-agent@^8.0.3:
version "8.0.4"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c"
integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==
version "8.0.5"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee"
integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==
dependencies:
agent-base "^7.1.1"
agent-base "^7.1.2"
debug "^4.3.4"
socks "^2.8.3"
socks@^2.8.3:
version "2.8.3"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5"
integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==
version "2.8.7"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea"
integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==
dependencies:
ip-address "^9.0.5"
ip-address "^10.0.1"
smart-buffer "^4.2.0"
sorted-btree@^1.8.1:
@ -9706,11 +9696,6 @@ split.js@^1.6.5:
resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.5.tgz#f7f61da1044c9984cb42947df4de4fadb5a3f300"
integrity sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==
sprintf-js@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -9959,9 +9944,9 @@ tapable@^2.2.1, tapable@^2.3.0:
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
tar@^7.4.3, tar@^7.5.2:
version "7.5.9"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8"
integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==
version "7.5.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"
@ -9970,14 +9955,13 @@ tar@^7.4.3, tar@^7.5.2:
yallist "^5.0.0"
terser-webpack-plugin@^5.3.16:
version "5.3.16"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"
integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==
version "5.4.0"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz#95fc4cf4437e587be11ecf37d08636089174d76b"
integrity sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
schema-utils "^4.3.0"
serialize-javascript "^6.0.2"
terser "^5.31.1"
terser@5.43.1:

Loading…
Cancel
Save