From 749ddb975f1752249597fa5c7cc43b6888bc9b67 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Sat, 5 Jul 2025 18:55:48 +0530 Subject: [PATCH] Support ApiServer to enforce POST requests for state changing APIs and requests with timestamps (#10899) Co-authored-by: Kevin Li --- .../apache/cloudstack/api/ApiErrorCode.java | 1 + .../cloudstack/api/ApiServerService.java | 2 + .../main/java/com/cloud/api/ApiServer.java | 26 +- .../main/java/com/cloud/api/ApiServlet.java | 115 ++++++- ui/src/api/index.js | 44 ++- .../components/header/SamlDomainSwitcher.vue | 6 +- ui/src/components/header/UserMenu.vue | 4 +- ui/src/components/page/GlobalLayout.vue | 4 +- ui/src/components/view/ActionButton.vue | 4 +- ui/src/components/view/AnnotationsTab.vue | 10 +- ui/src/components/view/DedicateData.vue | 18 +- ui/src/components/view/DedicateDomain.vue | 6 +- ui/src/components/view/DedicateModal.vue | 10 +- ui/src/components/view/DetailSettings.vue | 8 +- ui/src/components/view/EventsTab.vue | 4 +- .../components/view/ImageStoreSelectView.vue | 4 +- ui/src/components/view/InfoCard.vue | 16 +- ...stanceVolumesStoragePoolSelectListView.vue | 4 +- ui/src/components/view/ListResourceTable.vue | 4 +- ui/src/components/view/ListView.vue | 12 +- .../components/view/NicNetworkSelectForm.vue | 4 +- ui/src/components/view/ResourceLimitTab.vue | 6 +- ui/src/components/view/ResourceView.vue | 4 +- ui/src/components/view/SearchView.vue | 38 +-- ui/src/components/view/SettingsTab.vue | 4 +- ui/src/components/view/StatsTab.vue | 4 +- .../components/view/StoragePoolSelectView.vue | 6 +- .../view/TestWebhookDeliveryView.vue | 4 +- ui/src/components/view/TreeView.vue | 8 +- ui/src/components/view/UploadResourceIcon.vue | 6 +- ui/src/components/view/VmwareData.vue | 4 +- ui/src/components/view/VolumesTab.vue | 4 +- .../components/view/WebhookDeliveriesTab.vue | 12 +- ui/src/components/widgets/Console.vue | 4 +- .../widgets/InfiniteScrollSelect.vue | 4 +- ui/src/components/widgets/OsLogo.vue | 4 +- ui/src/store/modules/user.js | 30 +- ui/src/utils/plugins.js | 4 +- ui/src/views/AutogenView.vue | 21 +- ui/src/views/auth/ForgotPassword.vue | 4 +- ui/src/views/auth/Login.vue | 8 +- ui/src/views/auth/ResetPassword.vue | 4 +- ui/src/views/compute/AssignInstance.vue | 6 +- ui/src/views/compute/AttachIso.vue | 6 +- .../views/compute/AutoScaleDownPolicyTab.vue | 26 +- .../views/compute/AutoScaleLoadBalancing.vue | 26 +- ui/src/views/compute/AutoScaleUpPolicyTab.vue | 26 +- ui/src/views/compute/AutoScaleVmProfile.vue | 14 +- ui/src/views/compute/BackupScheduleWizard.vue | 4 +- ui/src/views/compute/ChangeAffinity.vue | 6 +- .../views/compute/CreateAutoScaleVmGroup.vue | 34 +-- .../views/compute/CreateKubernetesCluster.vue | 28 +- ui/src/views/compute/CreateSSHKeyPair.vue | 8 +- ui/src/views/compute/CreateSnapshotWizard.vue | 6 +- ui/src/views/compute/DeployVM.vue | 24 +- ui/src/views/compute/DeployVnfAppliance.vue | 22 +- ui/src/views/compute/DestroyVM.vue | 6 +- ui/src/views/compute/EditVM.vue | 22 +- ui/src/views/compute/InstanceSchedules.vue | 8 +- ui/src/views/compute/InstanceTab.vue | 8 +- ui/src/views/compute/KubernetesServiceTab.vue | 16 +- ui/src/views/compute/MigrateVMStorage.vue | 4 +- ui/src/views/compute/MigrateWizard.vue | 8 +- ui/src/views/compute/RegisterUserData.vue | 6 +- ui/src/views/compute/ReinstallVm.vue | 10 +- ui/src/views/compute/ResetSshKeyPair.vue | 6 +- ui/src/views/compute/ResetUserData.vue | 15 +- .../views/compute/ScaleKubernetesCluster.vue | 10 +- ui/src/views/compute/ScaleVM.vue | 10 +- ui/src/views/compute/StartVirtualMachine.vue | 10 +- .../compute/UpgradeKubernetesCluster.vue | 6 +- .../views/compute/backup/BackupSchedule.vue | 4 +- ui/src/views/compute/backup/FormSchedule.vue | 4 +- .../compute/wizard/MultiDiskSelection.vue | 4 +- .../compute/wizard/MultiNetworkSelection.vue | 4 +- .../views/compute/wizard/NetworkSelection.vue | 10 +- .../compute/wizard/OwnershipSelection.vue | 8 +- .../compute/wizard/SecurityGroupSelection.vue | 4 +- ui/src/views/dashboard/CapacityDashboard.vue | 26 +- ui/src/views/dashboard/Dashboard.vue | 4 +- ui/src/views/dashboard/SetupTwoFaAtLogin.vue | 12 +- ui/src/views/dashboard/UsageDashboard.vue | 30 +- ui/src/views/dashboard/VerifyOauth.vue | 4 +- ui/src/views/dashboard/VerifyTwoFa.vue | 4 +- ui/src/views/iam/AddAccount.vue | 12 +- ui/src/views/iam/AddLdapAccount.vue | 16 +- ui/src/views/iam/AddUser.vue | 12 +- ui/src/views/iam/ChangeUserPassword.vue | 4 +- ui/src/views/iam/ConfigureSamlSsoAuth.vue | 8 +- ui/src/views/iam/CreateRole.vue | 6 +- ui/src/views/iam/DeleteAccount.vue | 6 +- ui/src/views/iam/DomainActionForm.vue | 4 +- ui/src/views/iam/DomainView.vue | 31 +- ui/src/views/iam/EditAccount.vue | 6 +- ui/src/views/iam/EditUser.vue | 4 +- ui/src/views/iam/ImportRole.vue | 4 +- ui/src/views/iam/RolePermissionTab.vue | 12 +- ui/src/views/iam/SSLCertificateTab.vue | 6 +- ui/src/views/iam/SetupTwoFaAtUserProfile.vue | 12 +- .../image/AddKubernetesSupportedVersion.vue | 6 +- ui/src/views/image/IsoZones.vue | 12 +- ui/src/views/image/RegisterOrUploadIso.vue | 18 +- .../views/image/RegisterOrUploadTemplate.vue | 24 +- ui/src/views/image/TemplateVnfSettings.vue | 6 +- ui/src/views/image/TemplateZones.vue | 12 +- ui/src/views/image/UpdateISO.vue | 10 +- .../UpdateKubernetesSupportedVersion.vue | 4 +- ui/src/views/image/UpdateTemplate.vue | 10 +- .../image/UpdateTemplateIsoPermissions.vue | 12 +- ui/src/views/infra/AddObjectStorage.vue | 4 +- ui/src/views/infra/AddPrimaryStorage.vue | 16 +- ui/src/views/infra/AddSecondaryStorage.vue | 8 +- ui/src/views/infra/AsyncJobsTab.vue | 4 +- ui/src/views/infra/ChangeHostPassword.vue | 4 +- ui/src/views/infra/ChangeStoragePoolScope.vue | 6 +- ui/src/views/infra/ClusterAdd.vue | 14 +- ui/src/views/infra/ClusterDRSTab.vue | 12 +- ui/src/views/infra/ClusterUpdate.vue | 8 +- ui/src/views/infra/ConfigureHostOOBM.vue | 4 +- ui/src/views/infra/Confirmation.vue | 4 +- ui/src/views/infra/ConnectedAgentsTab.vue | 4 +- ui/src/views/infra/CpuSockets.vue | 4 +- ui/src/views/infra/HostAdd.vue | 14 +- ui/src/views/infra/HostEnableDisable.vue | 6 +- ui/src/views/infra/HostInfo.vue | 4 +- ui/src/views/infra/HostUpdate.vue | 10 +- ui/src/views/infra/InfraSummary.vue | 12 +- .../views/infra/ManagementServerPeerTab.vue | 4 +- ui/src/views/infra/Metrics.vue | 6 +- ui/src/views/infra/MigrateData.vue | 6 +- ui/src/views/infra/PodAdd.vue | 8 +- ui/src/views/infra/PodUpdate.vue | 8 +- ui/src/views/infra/Resources.vue | 4 +- ui/src/views/infra/StorageBrowser.vue | 8 +- ui/src/views/infra/UpdatePrimaryStorage.vue | 8 +- ui/src/views/infra/UsageRecords.vue | 22 +- ui/src/views/infra/ZoneUpdate.vue | 8 +- .../views/infra/network/DedicatedVLANTab.vue | 14 +- .../views/infra/network/EditTrafficLabel.vue | 6 +- .../views/infra/network/IpRangesTabGuest.vue | 10 +- .../infra/network/IpRangesTabManagement.vue | 8 +- .../views/infra/network/IpRangesTabPublic.vue | 18 +- .../infra/network/IpRangesTabStorage.vue | 10 +- .../infra/network/ServiceProvidersTab.vue | 12 +- .../views/infra/network/TrafficTypesTab.vue | 12 +- .../network/providers/AddF5LoadBalancer.vue | 6 +- .../providers/AddNetscalerLoadBalancer.vue | 6 +- .../network/providers/AddNiciraNvpDevice.vue | 6 +- .../network/providers/AddPaloAltoFirewall.vue | 6 +- .../infra/network/providers/ProviderItem.vue | 4 +- .../network/providers/ProviderListView.vue | 6 +- .../views/infra/routers/RouterHealthCheck.vue | 6 +- ui/src/views/infra/zone/AsNumbersTab.vue | 8 +- ui/src/views/infra/zone/BgpPeersTab.vue | 16 +- .../views/infra/zone/Ipv4GuestSubnetsTab.vue | 16 +- .../views/infra/zone/PhysicalNetworksTab.vue | 8 +- ui/src/views/infra/zone/SystemVmsTab.vue | 4 +- .../infra/zone/ZoneWizardAddResources.vue | 10 +- .../views/infra/zone/ZoneWizardLaunchZone.vue | 68 ++--- .../infra/zone/ZoneWizardNetworkSetupStep.vue | 4 +- .../infra/zone/ZoneWizardZoneDetailsStep.vue | 8 +- ui/src/views/network/AclListRulesTab.vue | 22 +- .../views/network/ChangeBgpPeerForNetwork.vue | 6 +- ui/src/views/network/ChangeBgpPeerForVpc.vue | 6 +- .../network/CreateIpv4SubnetForNetwork.vue | 8 +- .../network/CreateIsolatedNetworkForm.vue | 14 +- ui/src/views/network/CreateL2NetworkForm.vue | 10 +- ui/src/views/network/CreateNetwork.vue | 4 +- .../views/network/CreateNetworkPermission.vue | 4 +- .../views/network/CreateSharedNetworkForm.vue | 22 +- ui/src/views/network/CreateVlanIpRange.vue | 8 +- ui/src/views/network/CreateVpc.vue | 12 +- .../network/CreateVpnCustomerGateway.vue | 4 +- ui/src/views/network/EgressRulesTab.vue | 12 +- ui/src/views/network/EnableStaticNat.vue | 12 +- ui/src/views/network/FirewallRules.vue | 18 +- ui/src/views/network/GuestIpRanges.vue | 8 +- ui/src/views/network/GuestVlanNetworksTab.vue | 4 +- .../network/IngressEgressRuleConfigure.vue | 16 +- .../views/network/InternalLBAssignVmForm.vue | 10 +- .../views/network/InternalLBAssignedVmTab.vue | 6 +- ui/src/views/network/IpAddressesTab.vue | 14 +- ui/src/views/network/Ipv6FirewallRulesTab.vue | 8 +- ui/src/views/network/LoadBalancing.vue | 42 +-- ui/src/views/network/NetworkPermissions.vue | 8 +- ui/src/views/network/NicsTab.vue | 22 +- ui/src/views/network/NicsTable.vue | 4 +- ui/src/views/network/PortForwarding.vue | 20 +- ui/src/views/network/PublicIpResource.vue | 8 +- ui/src/views/network/ReservePublicIP.vue | 10 +- ui/src/views/network/RoutersTab.vue | 4 +- .../views/network/RoutingFirewallRulesTab.vue | 8 +- ui/src/views/network/StaticRoutesTab.vue | 14 +- ui/src/views/network/UpdateNetwork.vue | 8 +- ui/src/views/network/VnfAppliancesTab.vue | 4 +- ui/src/views/network/VpcTab.vue | 26 +- ui/src/views/network/VpcTiersTab.vue | 26 +- ui/src/views/network/VpnDetails.vue | 8 +- .../network/tungsten/FirewallPolicyTab.vue | 8 +- .../network/tungsten/FirewallRuleTab.vue | 18 +- .../views/network/tungsten/FirewallTagTab.vue | 10 +- .../network/tungsten/LogicalRouterTab.vue | 8 +- .../network/tungsten/NetworkPolicyTab.vue | 10 +- .../tungsten/TungstenFabricPolicyRule.vue | 8 +- .../tungsten/TungstenFabricPolicyTag.vue | 10 +- .../tungsten/TungstenFabricTableView.vue | 10 +- ui/src/views/offering/AddComputeOffering.vue | 18 +- ui/src/views/offering/AddDiskOffering.vue | 14 +- ui/src/views/offering/AddNetworkOffering.vue | 18 +- ui/src/views/offering/AddVpcOffering.vue | 14 +- .../views/offering/ImportBackupOffering.vue | 8 +- .../views/offering/UpdateOfferingAccess.vue | 10 +- ui/src/views/plugins/ApiDocsPlugin.vue | 4 +- ui/src/views/plugins/CloudianPlugin.vue | 4 +- .../views/plugins/quota/CreateQuotaTariff.vue | 6 +- .../views/plugins/quota/EditQuotaTariff.vue | 6 +- .../plugins/quota/EditTariffValueWizard.vue | 4 +- .../plugins/quota/EmailTemplateDetails.vue | 6 +- ui/src/views/plugins/quota/QuotaBalance.vue | 6 +- .../plugins/quota/QuotaSummaryResource.vue | 4 +- ui/src/views/plugins/quota/QuotaUsage.vue | 6 +- ui/src/views/project/AccountsTab.vue | 12 +- .../project/AddAccountOrUserToProject.vue | 12 +- .../views/project/InvitationTokenTemplate.vue | 4 +- ui/src/views/project/InvitationsTemplate.vue | 6 +- .../project/iam/ProjectRolePermissionTab.vue | 12 +- ui/src/views/project/iam/ProjectRoleTab.vue | 10 +- ui/src/views/setting/ConfigurationTab.vue | 6 +- ui/src/views/setting/ConfigurationValue.vue | 6 +- ui/src/views/storage/AttachVolume.vue | 6 +- .../views/storage/ChangeOfferingForVolume.vue | 6 +- .../storage/ChangeSharedFSDiskOffering.vue | 6 +- .../storage/ChangeSharedFSServiceOffering.vue | 8 +- ui/src/views/storage/CreateBucket.vue | 6 +- ui/src/views/storage/CreateSharedFS.vue | 12 +- .../storage/CreateSnapshotFromVMSnapshot.vue | 6 +- ui/src/views/storage/CreateTemplate.vue | 14 +- ui/src/views/storage/CreateVolume.vue | 14 +- ui/src/views/storage/FormSchedule.vue | 6 +- .../storage/MigrateImageStoreResource.vue | 6 +- ui/src/views/storage/MigrateVolume.vue | 10 +- .../views/storage/RecurringSnapshotVolume.vue | 4 +- ui/src/views/storage/ResizeVolume.vue | 6 +- .../storage/RestoreAttachBackupVolume.vue | 6 +- ui/src/views/storage/ScheduledSnapshots.vue | 4 +- ui/src/views/storage/SharedFSTab.vue | 6 +- ui/src/views/storage/SnapshotZones.vue | 10 +- ui/src/views/storage/TakeSnapshot.vue | 6 +- ui/src/views/storage/UpdateBucket.vue | 4 +- ui/src/views/storage/UpdateSharedFS.vue | 4 +- ui/src/views/storage/UploadLocalVolume.vue | 12 +- ui/src/views/storage/UploadVolume.vue | 12 +- ui/src/views/tools/CreateWebhook.vue | 8 +- .../views/tools/ImportUnmanagedInstance.vue | 16 +- ui/src/views/tools/ManageInstances.vue | 14 +- ui/src/views/tools/ManageVolumes.vue | 20 +- ui/src/views/tools/SelectVmwareVcenter.vue | 8 +- ui/tests/common/index.js | 10 + .../unit/components/view/ActionButton.spec.js | 30 +- ui/tests/unit/views/AutogenView.spec.js | 286 ++++++++---------- .../unit/views/compute/MigrateWizard.spec.js | 38 +-- 261 files changed, 1581 insertions(+), 1478 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiErrorCode.java b/api/src/main/java/org/apache/cloudstack/api/ApiErrorCode.java index 03dc37325d4..616c37484d8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiErrorCode.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiErrorCode.java @@ -22,6 +22,7 @@ package org.apache.cloudstack.api; */ public enum ApiErrorCode { + BAD_REQUEST(400), UNAUTHORIZED(401), UNAUTHORIZED2FA(511), METHOD_NOT_ALLOWED(405), diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java b/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java index cbbcdc3bda4..cb75939d6bc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java @@ -48,4 +48,6 @@ public interface ApiServerService { boolean forgotPassword(UserAccount userAccount, Domain domain); boolean resetPassword(UserAccount userAccount, String token, String password); + + boolean isPostRequestsAndTimestampsEnforced(); } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index b8227ef9d58..34752000907 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -201,6 +201,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer private static final String SANITIZATION_REGEX = "[\n\r]"; private static boolean encodeApiResponse = false; + private boolean isPostRequestsAndTimestampsEnforced = false; /** * Non-printable ASCII characters - numbers 0 to 31 and 127 decimal @@ -284,6 +285,13 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer , "Session cookie is marked as secure if this is enabled. Secure cookies only work when HTTPS is used." , false , ConfigKey.Scope.Global); + static final ConfigKey EnforcePostRequestsAndTimestamps = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED + , Boolean.class + , "enforce.post.requests.and.timestamps" + , "false" + , "Enable/Disable whether the ApiServer should only accept POST requests for state-changing APIs and requests with timestamps." + , false + , ConfigKey.Scope.Global); private static final ConfigKey JSONDefaultContentType = new ConfigKey<> (ConfigKey.CATEGORY_ADVANCED , String.class , "json.content.type" @@ -441,6 +449,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer public boolean start() { Security.addProvider(new BouncyCastleProvider()); Integer apiPort = IntegrationAPIPort.value(); // api port, null by default + isPostRequestsAndTimestampsEnforced = EnforcePostRequestsAndTimestamps.value(); final Long snapshotLimit = ConcurrentSnapshotsThresholdPerHost.value(); if (snapshotLimit == null || snapshotLimit <= 0) { @@ -720,6 +729,11 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer return response; } + @Override + public boolean isPostRequestsAndTimestampsEnforced() { + return isPostRequestsAndTimestampsEnforced; + } + private String getBaseAsyncResponse(final long jobId, final BaseAsyncCmd cmd) { final AsyncJobResponse response = new AsyncJobResponse(); @@ -967,7 +981,6 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer // put the name in a list that we'll sort later final List parameterNames = new ArrayList<>(requestParameters.keySet()); - Collections.sort(parameterNames); String signatureVersion = null; @@ -1019,12 +1032,22 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer } final Date now = new Date(System.currentTimeMillis()); + final Date thresholdTime = new Date(now.getTime() + 15 * 60 * 1000); if (expiresTS.before(now)) { signature = signature.replaceAll(SANITIZATION_REGEX, "_"); apiKey = apiKey.replaceAll(SANITIZATION_REGEX, "_"); logger.debug("Request expired -- ignoring ...sig [{}], apiKey [{}].", signature, apiKey); return false; + } else if (isPostRequestsAndTimestampsEnforced && expiresTS.after(thresholdTime)) { + signature = signature.replaceAll(SANITIZATION_REGEX, "_"); + apiKey = apiKey.replaceAll(SANITIZATION_REGEX, "_"); + logger.debug(String.format("Expiration parameter is set for too long -- ignoring ...sig [%s], apiKey [%s].", signature, apiKey)); + return false; } + } else if (isPostRequestsAndTimestampsEnforced) { + // Force expiration parameter + logger.debug("Signature Version must be 3, and should be along with the Expires parameter -- ignoring request."); + return false; } final TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); @@ -1648,6 +1671,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] { + EnforcePostRequestsAndTimestamps, IntegrationAPIPort, ConcurrentSnapshotsThresholdPerHost, EncodeApiResponse, diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 4994c42bb4d..41229670c38 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -22,8 +22,11 @@ import java.net.URLDecoder; import java.net.UnknownHostException; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; +import java.util.Set; import javax.inject.Inject; import javax.servlet.ServletConfig; @@ -46,6 +49,7 @@ import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpoint import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.managed.context.ManagedContext; import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils; +import org.apache.commons.collections.MapUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -78,6 +82,39 @@ public class ApiServlet extends HttpServlet { private static final Logger ACCESSLOGGER = LogManager.getLogger("apiserver." + ApiServlet.class.getName()); private static final String REPLACEMENT = "_"; private static final String LOGGER_REPLACEMENTS = "[\n\r\t]"; + private static final Pattern GET_REQUEST_COMMANDS = Pattern.compile("^(get|list|query|find)(\\w+)+$"); + private static final HashSet GET_REQUEST_COMMANDS_LIST = new HashSet<>(Set.of("isaccountallowedtocreateofferingswithtags", + "readyforshutdown", "cloudianisenabled", "quotabalance", "quotasummary", "quotatarifflist", "quotaisenabled", "quotastatement", "verifyoauthcodeandgetuser")); + private static final HashSet POST_REQUESTS_TO_DISABLE_LOGGING = new HashSet<>(Set.of( + "login", + "oauthlogin", + "createaccount", + "createuser", + "updateuser", + "forgotpassword", + "resetpassword", + "importrole", + "updaterolepermission", + "updateprojectrolepermission", + "createstoragepool", + "addhost", + "updatehostpassword", + "addcluster", + "addvmwaredc", + "configureoutofbandmanagement", + "uploadcustomcertificate", + "addciscovnmcresource", + "addnetscalerloadbalancer", + "createtungstenfabricprovider", + "addnsxcontroller", + "configtungstenfabricservice", + "createnetworkacl", + "updatenetworkaclitem", + "quotavalidateactivationrule", + "quotatariffupdate", + "listandswitchsamlaccount", + "uploadresourceicon" + )); @Inject ApiServerService apiServer; @@ -193,11 +230,24 @@ public class ApiServlet extends HttpServlet { utf8Fixup(req, params); + final Object[] commandObj = params.get(ApiConstants.COMMAND); + final String command = commandObj == null ? null : (String) commandObj[0]; + // logging the request start and end in management log for easy debugging String reqStr = ""; String cleanQueryString = StringUtils.cleanString(req.getQueryString()); if (LOGGER.isDebugEnabled()) { reqStr = auditTrailSb.toString() + " " + cleanQueryString; + if (req.getMethod().equalsIgnoreCase("POST") && org.apache.commons.lang3.StringUtils.isNotBlank(command)) { + if (!POST_REQUESTS_TO_DISABLE_LOGGING.contains(command.toLowerCase()) && !reqParams.containsKey(ApiConstants.USER_DATA)) { + String cleanParamsString = getCleanParamsString(reqParams); + if (org.apache.commons.lang3.StringUtils.isNotBlank(cleanParamsString)) { + reqStr += "\n" + cleanParamsString; + } + } else { + reqStr += " " + command; + } + } LOGGER.debug("===START=== " + reqStr); } @@ -213,8 +263,6 @@ public class ApiServlet extends HttpServlet { responseType = (String)responseTypeParam[0]; } - final Object[] commandObj = params.get(ApiConstants.COMMAND); - final String command = commandObj == null ? null : (String) commandObj[0]; final Object[] userObj = params.get(ApiConstants.USERNAME); String username = userObj == null ? null : (String)userObj[0]; if (LOGGER.isTraceEnabled()) { @@ -317,6 +365,19 @@ public class ApiServlet extends HttpServlet { } } + if (apiServer.isPostRequestsAndTimestampsEnforced() && !isStateChangingCommandUsingPOST(command, req.getMethod(), params)) { + String errorText = String.format("State changing command %s needs to be sent using POST request", command); + if (command.equalsIgnoreCase("updateConfiguration") && params.containsKey("name")) { + errorText = String.format("Changes for configuration %s needs to be sent using POST request", params.get("name")[0]); + } + auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + errorText); + final String serializedResponse = + apiServer.getSerializedApiError(new ServerApiException(ApiErrorCode.BAD_REQUEST, errorText), params, + responseType); + HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType, ApiServer.JSONcontentType.value()); + return; + } + Long userId = null; if (!isNew) { userId = (Long)session.getAttribute("userid"); @@ -407,6 +468,15 @@ public class ApiServlet extends HttpServlet { return verify2FA; } + private boolean isStateChangingCommandUsingPOST(String command, String method, Map params) { + if (command == null || (!GET_REQUEST_COMMANDS.matcher(command.toLowerCase()).matches() && !GET_REQUEST_COMMANDS_LIST.contains(command.toLowerCase()) + && !command.equalsIgnoreCase("updateConfiguration") && !method.equals("POST"))) { + return false; + } + return !command.equalsIgnoreCase("updateConfiguration") || method.equals("POST") || (params.containsKey("name") + && params.get("name")[0].toString().equalsIgnoreCase(ApiServer.EnforcePostRequestsAndTimestamps.key())); + } + protected boolean skip2FAcheckForAPIs(String command) { boolean skip2FAcheck = false; @@ -644,4 +714,45 @@ public class ApiServlet extends HttpServlet { } return null; } + + private String getCleanParamsString(Map reqParams) { + if (MapUtils.isEmpty(reqParams)) { + return ""; + } + + StringBuilder cleanParamsString = new StringBuilder(); + for (Map.Entry reqParam : reqParams.entrySet()) { + if (org.apache.commons.lang3.StringUtils.isBlank(reqParam.getKey())) { + continue; + } + + cleanParamsString.append(reqParam.getKey()); + cleanParamsString.append("="); + + if (reqParam.getKey().toLowerCase().contains("password") + || reqParam.getKey().toLowerCase().contains("privatekey") + || reqParam.getKey().toLowerCase().contains("accesskey") + || reqParam.getKey().toLowerCase().contains("secretkey")) { + cleanParamsString.append("\n"); + continue; + } + + if (reqParam.getValue() == null || reqParam.getValue().length == 0) { + cleanParamsString.append("\n"); + continue; + } + + for (String param : reqParam.getValue()) { + if (org.apache.commons.lang3.StringUtils.isBlank(param)) { + continue; + } + String cleanParamString = StringUtils.cleanString(param.trim()); + cleanParamsString.append(cleanParamString); + cleanParamsString.append(" "); + } + cleanParamsString.append("\n"); + } + + return cleanParamsString.toString(); + } } diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 7ab87780a9d..85c46c483b2 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -23,18 +23,10 @@ import { ACCESS_TOKEN } from '@/store/mutation-types' -export function api (command, args = {}, method = 'GET', data = {}) { - let params = {} +export function getAPI (command, args = {}) { args.command = command args.response = 'json' - if (data) { - params = new URLSearchParams() - Object.entries(data).forEach(([key, value]) => { - params.append(key, value) - }) - } - const sessionkey = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('sessionkey') if (sessionkey) { args.sessionkey = sessionkey @@ -45,8 +37,30 @@ export function api (command, args = {}, method = 'GET', data = {}) { ...args }, url: '/', - method, - data: params || {} + method: 'GET' + }) +} + +export function postAPI (command, data = {}) { + const params = new URLSearchParams() + params.append('command', command) + params.append('response', 'json') + if (data) { + Object.entries(data).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, value) + } + }) + } + + const sessionkey = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('sessionkey') + if (sessionkey) { + params.append('sessionkey', sessionkey) + } + return axios({ + url: '/', + method: 'POST', + data: params }) } @@ -56,7 +70,7 @@ export function login (arg) { } // Logout before login is called to purge any duplicate sessionkey cookies - api('logout') + postAPI('logout') const params = new URLSearchParams() params.append('command', 'login') @@ -66,7 +80,7 @@ export function login (arg) { params.append('response', 'json') return axios({ url: '/', - method: 'post', + method: 'POST', data: params, headers: { 'content-type': 'application/x-www-form-urlencoded' @@ -77,7 +91,7 @@ export function login (arg) { export function logout () { message.destroy() notification.destroy() - return api('logout') + return postAPI('logout') } export function oauthlogin (arg) { @@ -86,7 +100,7 @@ export function oauthlogin (arg) { } // Logout before login is called to purge any duplicate sessionkey cookies - api('logout') + postAPI('logout') const params = new URLSearchParams() params.append('command', 'oauthlogin') diff --git a/ui/src/components/header/SamlDomainSwitcher.vue b/ui/src/components/header/SamlDomainSwitcher.vue index 082bab7bf13..e0799bef4d8 100644 --- a/ui/src/components/header/SamlDomainSwitcher.vue +++ b/ui/src/components/header/SamlDomainSwitcher.vue @@ -51,7 +51,7 @@