feat(azure): add check to ensure Function Apps redirect HTTP to HTTPS (#11929)

Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
Aman
2026-07-10 18:27:58 +05:30
committed by GitHub
parent a4bf70eecf
commit a0a0883578
7 changed files with 267 additions and 0 deletions
@@ -0,0 +1 @@
`app_function_ensure_http_is_redirected_to_https` check for Azure provider, verifying that Function Apps enforce HTTPS-only traffic
@@ -0,0 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_ensure_http_is_redirected_to_https",
"CheckTitle": "Function app redirects HTTP to HTTPS",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "**Azure Function apps** redirect `HTTP` traffic to `HTTPS` when the `HTTPS Only` setting is enabled. This evaluation identifies Function apps that do not force secure transport by checking whether plaintext requests are automatically redirected to encrypted endpoints.",
"Risk": "Leaving **HTTP accessible** on a Function app enables **man-in-the-middle** interception, credential and token theft, and response tampering. This undermines **confidentiality** and **integrity**, and can lead to session hijacking or downgrade attacks that bypass TLS.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-https",
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-3-encrypt-sensitive-data-in-transit"
],
"Remediation": {
"Code": {
"CLI": "az functionapp update --resource-group <RESOURCE_GROUP_NAME> --name <FUNCTION_APP_NAME> --https-only true",
"NativeIaC": "```bicep\n// Enable HTTPS-only redirect on an existing Function App\nresource functionApp 'Microsoft.Web/sites@2022-09-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n httpsOnly: true // Critical: forces redirect from HTTP to HTTPS\n }\n}\n```",
"Other": "1. Sign in to the Azure portal and go to Function Apps\n2. Select your Function app\n3. Go to TLS/SSL settings and set HTTPS Only to On\n4. Click Save",
"Terraform": "```hcl\n# Enforce HTTPS-only on a Function App\nresource \"azurerm_linux_function_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n service_plan_id = \"<example_resource_id>\"\n storage_account_name = \"<example_resource_name>\"\n storage_account_access_key = \"<example_secret_value>\"\n\n https_only = true # Critical: redirects HTTP to HTTPS\n}\n```"
},
"Recommendation": {
"Text": "Enforce **HTTPS-only** for all Function apps.\n- Use trusted certificates and require `TLS 1.2` or later\n- Enable **HSTS** to prevent downgrade/mixed-content\n- Redirect legacy `http` links to `https`\n- Minimize HTTP exposure via WAF/CDN or private access\nApply **defense in depth** to protect data in transit.",
"Url": "https://hub.prowler.com/check/app_function_ensure_http_is_redirected_to_https"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check mirrors app_ensure_http_is_redirected_to_https but evaluates Function apps (Microsoft.Web/sites with kind starting with 'functionapp') via app_client.functions. When HTTPS Only is enabled, every incoming HTTP request is redirected to the HTTPS port."
}
@@ -0,0 +1,35 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.app.app_client import app_client
class app_function_ensure_http_is_redirected_to_https(Check):
"""Ensure Function Apps redirect HTTP traffic to HTTPS."""
def execute(self) -> list[Check_Report_Azure]:
"""Execute the check logic.
Returns:
A list of reports for Function Apps HTTPS-only enforcement.
"""
findings = []
for (
subscription_id,
functions,
) in app_client.functions.items():
subscription_name = app_client.subscriptions.get(
subscription_id, subscription_id
)
for function in functions.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=function)
report.subscription = subscription_id
report.status = "PASS"
report.status_extended = f"HTTP is redirected to HTTPS for Function app '{function.name}' in subscription '{subscription_name} ({subscription_id})'."
if not function.https_only:
report.status = "FAIL"
report.status_extended = f"HTTP is not redirected to HTTPS for Function app '{function.name}' in subscription '{subscription_name} ({subscription_id})'."
findings.append(report)
return findings
@@ -178,6 +178,7 @@ class App(AzureService):
ftps_state=getattr(
function_config, "ftps_state", None
),
https_only=getattr(function, "https_only", False),
)
}
)
@@ -301,3 +302,4 @@ class FunctionApp:
public_access: bool
vnet_subnet_id: str
ftps_state: Optional[str]
https_only: bool = False
@@ -0,0 +1,157 @@
from unittest import mock
from uuid import uuid4
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
AZURE_SUBSCRIPTION_NAME,
set_mocked_azure_provider,
)
class Test_app_function_ensure_http_is_redirected_to_https:
def test_function_no_subscriptions(self):
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.functions = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
app_function_ensure_http_is_redirected_to_https,
)
check = app_function_ensure_http_is_redirected_to_https()
result = check.execute()
assert len(result) == 0
def test_function_subscriptions_empty(self):
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.functions = {AZURE_SUBSCRIPTION_ID: {}}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
app_function_ensure_http_is_redirected_to_https,
)
check = app_function_ensure_http_is_redirected_to_https()
result = check.execute()
assert len(result) == 0
def test_function_http_not_redirected(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
app_function_ensure_http_is_redirected_to_https,
)
from prowler.providers.azure.services.app.app_service import FunctionApp
app_client.functions = {
AZURE_SUBSCRIPTION_ID: {
resource_id: FunctionApp(
id=resource_id,
name="function-1",
location="West Europe",
kind="functionapp",
function_keys=None,
environment_variables=None,
identity=None,
public_access=True,
vnet_subnet_id="",
ftps_state="Disabled",
https_only=False,
)
}
}
check = app_function_ensure_http_is_redirected_to_https()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"HTTP is not redirected to HTTPS for Function app 'function-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_name == "function-1"
assert result[0].resource_id == resource_id
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_function_http_redirected(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
app_function_ensure_http_is_redirected_to_https,
)
from prowler.providers.azure.services.app.app_service import FunctionApp
app_client.functions = {
AZURE_SUBSCRIPTION_ID: {
resource_id: FunctionApp(
id=resource_id,
name="function-1",
location="West Europe",
kind="functionapp",
function_keys=None,
environment_variables=None,
identity=None,
public_access=True,
vnet_subnet_id="",
ftps_state="Disabled",
https_only=True,
)
}
}
check = app_function_ensure_http_is_redirected_to_https()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"HTTP is redirected to HTTPS for Function app 'function-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_name == "function-1"
assert result[0].resource_id == resource_id
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
@@ -570,3 +570,38 @@ class Test_App_get_functions:
mock_client.web_apps.list_by_resource_group.assert_called_once_with(
resource_group_name="RG"
)
def test_get_functions_sets_https_only(self):
from prowler.providers.azure.services.app.app_service import App
mock_client = MagicMock()
mock_function = MagicMock()
mock_function.id = "/subscriptions/resource_id"
mock_function.name = "functionapp-1"
mock_function.location = "West Europe"
mock_function.kind = "functionapp"
mock_function.resource_group = RESOURCE_GROUP
mock_function.identity = None
mock_function.public_network_access = "Enabled"
mock_function.virtual_network_subnet_id = ""
mock_function.https_only = True
mock_client.web_apps.list.return_value = [mock_function]
app = object.__new__(App)
app.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
app.resource_groups = None
app._get_function_host_keys = MagicMock(return_value=None)
app._list_application_settings = MagicMock(
return_value=MagicMock(properties={})
)
app._get_function_config = MagicMock(
return_value=MagicMock(ftps_state="FtpsOnly")
)
result = app._get_functions()
function = result[AZURE_SUBSCRIPTION_ID][mock_function.id]
assert function.https_only is True
app._get_function_host_keys.assert_called_once_with(
AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP, "functionapp-1"
)