Skip to content

Commit fdb2f43

Browse files
Security updates (#188)
1 parent a70bda5 commit fdb2f43

20 files changed

Lines changed: 477 additions & 32 deletions

.github/workflows/dependency-review.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ jobs:
2626
# ISC: pexpect, ptyprocess
2727
# LGPL-3.0-or-later: pyzmq (libzmq shared library)
2828
# HPND: pillow
29+
# Python-2.0, GPL-1.0-or-later, 0BSD: typing-extensions (CPython compound license)
2930
allow-licenses: >-
3031
MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause,
3132
LGPL-2.0-or-later, LGPL-2.1-only, LGPL-3.0-or-later,
3233
MPL-1.1, MPL-2.0,
3334
OFL-1.1,
3435
PSF-2.0,
3536
ISC,
36-
HPND
37+
HPND,
38+
Python-2.0,
39+
GPL-1.0-or-later,
40+
0BSD
41+
42+
# ✅ Packages whose license cannot be auto-detected
43+
# cyclonedx-python-lib is Apache-2.0 (https://github.com/CycloneDX/cyclonedx-python-lib)
44+
allow-dependencies-licenses: "pkg:pypi/cyclonedx-python-lib"

.github/workflows/python-tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ jobs:
6464
run: |
6565
uv run python -m compileall infrastructure samples setup shared
6666
67+
# Supply-chain vulnerability audit (non-blocking)
68+
- name: Run pip-audit
69+
continue-on-error: true
70+
run: |
71+
uv run pip-audit
72+
6773
# Run tests and generate coverage reports
6874
- name: Run pytest with coverage and generate JUnit XML
6975
id: pytest

SECURITY.md

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
22

3-
## Security
3+
# Security
44

55
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations.
66

@@ -16,13 +16,13 @@ You should receive a response within 24 hours. If for some reason you do not, pl
1616

1717
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
1818

19-
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
20-
* Full paths of source file(s) related to the manifestation of the issue
21-
* The location of the affected source code (tag/branch/commit or direct URL)
22-
* Any special configuration required to reproduce the issue
23-
* Step-by-step instructions to reproduce the issue
24-
* Proof-of-concept or exploit code (if possible)
25-
* Impact of the issue, including how an attacker might exploit the issue
19+
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
20+
* Full paths of source file(s) related to the manifestation of the issue
21+
* The location of the affected source code (tag/branch/commit or direct URL)
22+
* Any special configuration required to reproduce the issue
23+
* Step-by-step instructions to reproduce the issue
24+
* Proof-of-concept or exploit code (if possible)
25+
* Impact of the issue, including how an attacker might exploit the issue
2626

2727
This information will help us triage your report more quickly.
2828

@@ -38,6 +38,60 @@ Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https:
3838

3939
<!-- END MICROSOFT SECURITY.MD BLOCK -->
4040

41+
## Repository Security Stance
42+
43+
This repository is a **learning playground** for Azure API Management. Its security posture reflects a deliberate balance: enforce real-world best practices wherever they do not impede the learning workflow, and clearly document every intentional relaxation so users know what to tighten for production.
44+
45+
### What We Enforce
46+
47+
#### Supply chain and CI
48+
49+
* GitHub Actions use least-privilege `permissions:` blocks, SHA-pinned third-party actions, `persist-credentials: false` on `actions/checkout`, and no `pull_request_target`.
50+
* Dependabot, Dependency Review, and OpenSSF Scorecard are configured.
51+
* `pip-audit` runs in CI as a non-blocking supply-chain vulnerability check.
52+
* `uv.lock` provides reproducible builds; `pyproject.toml` pins minimum versions for known-vulnerable packages.
53+
54+
#### Secrets hygiene
55+
56+
* No real secrets are committed. `.gitignore` and `.dockerignore` coverage is comprehensive.
57+
* Managed identity and APIM named values are used for backend credentials; no hardcoded keys in policies.
58+
* APIM subscription keys in Bicep outputs are annotated with `#disable-next-line outputs-should-not-contain-secrets` and an inline comment explaining intent. The Bicep linter rule is set to `warning` globally so that any *unintentional* secret output is caught.
59+
* Python helpers redact sensitive headers (`api-key`, `Authorization`, `Ocp-Apim-Subscription-Key`, `x-api-key`) before logging. The `print_secret()` utility masks secret values in notebook output.
60+
* The Azure CLI wrapper's secret-redaction regex covers access tokens, refresh tokens, client secrets, subscription keys, connection strings, storage account keys, and shared access signatures.
61+
62+
#### Authentication and authorization
63+
64+
* JWT validation via `validate-jwt` and `validate-azure-ad-token` is demonstrated in the `authX`, `authX-pro`, and `oauth-3rd-party` samples.
65+
* APIM policy error handlers return generic messages to callers; detailed diagnostics are emitted to Application Insights via `<trace>`.
66+
67+
#### Development tooling
68+
69+
* Local preview servers (`serve_website.py`, `serve_presentation.py`) bind to `127.0.0.1`, not `0.0.0.0`.
70+
* Jupyter notebook cell outputs are cleared before commit to prevent leaking resource names or subscription IDs.
71+
72+
### Intentional Compromises for Learning
73+
74+
The following defaults prioritise a frictionless learning experience. **Every one of them is parameterised** and can be flipped to the secure setting for production use.
75+
76+
| Default | Why | Production guidance |
77+
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
78+
| `apimPublicAccess = true` | Lets learners call APIM from their machine without VPN or private networking setup. | Disable public access; use Private Endpoints or VNet integration. |
79+
| `useStrictNsg = false` | Avoids NSG rules that block notebook or CLI connectivity during experiments. | Enable strict NSGs scoped to required ports and sources. |
80+
| WAF in `Detection` mode | Prevents the WAF from blocking exploratory requests while learners iterate. | Switch to `Prevention` mode once rules are tuned. |
81+
| `enablePurgeProtection = false` on Key Vault | Allows quick tear-down of lab environments without waiting for the purge-protection retention period. | Enable purge protection. |
82+
| `revealBackendApiInfo = true` (X-Backend-URL header) | Helps learners observe routing decisions and backend selection in responses. | Remove or disable the header; do not expose internal URLs. |
83+
| Subscription-key-only auth on admin APIs | Keeps sample setup simple. Each admin API includes a `SECURITY` comment pointing to `authX`/`authX-pro`. | Layer JWT validation (`validate-azure-ad-token` or `validate-jwt`). |
84+
| APIM subscription keys returned in Bicep outputs | Notebooks need keys to generate test traffic. Outputs use `#disable-next-line` with an intent comment. | Fetch keys via RBAC-controlled mechanisms; remove them from outputs. |
85+
86+
### What Is Not a Vulnerability
87+
88+
These items were reviewed and intentionally not flagged:
89+
90+
* **`allowInsecureTls=True` for Application Gateway infras** -- correctly scoped to AppGW infrastructure types (self-signed certificate by design); the flag is `False` for all other infrastructures.
91+
* **`shell=True` in Azure CLI wrappers** -- commands are constructed from controlled internal strings, never from user-supplied input.
92+
* **Application Insights instrumentation keys in Bicep outputs** -- Microsoft treats these as non-secret connection identifiers.
93+
* **`ast.literal_eval` fallback in JSON parsing** -- this is the safe `ast` module function, not `eval`.
94+
4195
## Security Scanning Scope
4296

4397
This repository is scanned by [OpenSSF Scorecard](https://github.com/ossf/scorecard) via a scheduled GitHub Action. Some checks will report a low score by design; the rationale is recorded as maintainer annotations in [`.github/scorecard.yml`](.github/scorecard.yml) and summarised below.
@@ -48,8 +102,8 @@ This repository does not implement dedicated fuzz testing, and the Scorecard Fuz
48102

49103
Fuzz testing is most valuable where code parses untrusted, attacker-controlled input — file formats, network protocols, deserialisers — particularly in memory-unsafe languages. This repository is a learning playground composed of Bicep templates, Jupyter notebooks, APIM policy XML, and thin Python wrappers around the Azure CLI. None of these components parse untrusted input locally:
50104

51-
- Bicep, policy XML, and notebooks are declarative assets consumed by Azure-side tooling, not by code in this repository.
52-
- The Python helpers read output from the operator's own `az` CLI session and their own policy files.
53-
- The only parsing surface (`shared/python/json_utils.py`) delegates to the Python standard library `json` and `ast` modules, which are [already fuzzed upstream in CPython via OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/cpython3).
105+
* Bicep, policy XML, and notebooks are declarative assets consumed by Azure-side tooling, not by code in this repository.
106+
* The Python helpers read output from the operator's own `az` CLI session and their own policy files.
107+
* The only parsing surface (`shared/python/json_utils.py`) delegates to the Python standard library `json` and `ast` modules, which are [already fuzzed upstream in CPython via OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/cpython3).
54108

55109
Scorecard additionally has no Python-native fuzzer detection — only OSS-Fuzz enrolment, ClusterFuzzLite, or language-native fuzzers for Go, Haskell, JavaScript/TypeScript, and Erlang are recognised. Adding `hypothesis` or `atheris` property-based tests would therefore not change the score, and would only exercise standard-library code paths already covered upstream.

bicepconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"level": "off"
1111
},
1212
"outputs-should-not-contain-secrets": {
13-
"level": "off"
13+
"level": "warning"
1414
}
1515
}
1616
}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dev = [
2727
"pytest>=9.0.0",
2828
"pytest-cov>=7.0.0",
2929
"coverage>=7.6.4",
30+
"pip-audit>=2.7.0",
3031
]
3132

3233
[tool.ruff]

samples/azure-maps/map_default_route_v2_aad_get.xml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,16 @@
4343
<cache-store-value key="azure-maps-sas-token" value="@((string)context.Variables["sas-token"])" duration="3600" />
4444
</when>
4545
<otherwise>
46-
<!-- Log error and return error response -->
47-
<set-variable name="error-message" value="@("Failed to retrieve SAS token. Status: " + ((IResponse)context.Variables["sas-response"]).StatusCode.ToString() + ", Body: " + ((IResponse)context.Variables["sas-response"]).Body.As<string>())" />
46+
<!-- Log detailed error to App Insights; return only a generic message to the caller -->
47+
<trace source="AzureMapsPolicy" severity="error">
48+
@("SAS token request failed. Status: " + ((IResponse)context.Variables["sas-response"]).StatusCode.ToString() + ", Body: " + ((IResponse)context.Variables["sas-response"]).Body.As<string>())
49+
</trace>
4850
<return-response>
4951
<set-status code="500" reason="Internal Server Error" />
5052
<set-header name="Content-Type" exists-action="override">
5153
<value>application/json</value>
5254
</set-header>
53-
<set-body>@{
54-
return new JObject(
55-
new JProperty("error", "SAS token generation failed"),
56-
new JProperty("details", (string)context.Variables["error-message"])
57-
).ToString();
58-
}</set-body>
55+
<set-body>{"error": "SAS token generation failed"}</set-body>
5956
</return-response>
6057
</otherwise>
6158
</choose>

samples/costing/main.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ output costExportName string = costExportOutputName
305305
@description('Subscription keys for the business units')
306306
output subscriptionKeys array = [for (bu, i) in businessUnits: {
307307
name: bu.name
308+
#disable-next-line outputs-should-not-contain-secrets // Intentional: notebook needs keys for traffic generation
308309
primaryKey: listSecrets(subscriptions[i].id, '2024-06-01-preview').primaryKey
309310
}]
310311

samples/costing/pf-extract-caller-id.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
back to the APIM subscription ID, or 'unknown' when neither is
1010
available.
1111
12+
IMPORTANT: This fragment reads the JWT payload WITHOUT verifying the
13+
signature. It assumes a parent policy (e.g. validate-jwt or
14+
validate-azure-ad-token) has already validated the token before this
15+
fragment executes. Do not use this fragment without upstream JWT
16+
validation.
17+
1218
Usage:
1319
<include-fragment fragment-id="Extract-CallerId" />
1420
After inclusion, use @((string)context.Variables["callerId"])

samples/dynamic-cors/admin-clear-cache.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
<base />
1919
<!-- Extract the cache key from the URL path parameter -->
2020
<set-variable name="cacheKey" value="@(context.Request.MatchedParameters["cacheKey"])" />
21+
<!-- Validate cache key format: alphanumeric, hyphens, underscores, max 64 characters -->
22+
<choose>
23+
<when condition="@(!System.Text.RegularExpressions.Regex.IsMatch((string)context.Variables["cacheKey"], @"^[A-Za-z0-9_\-]{1,64}$"))">
24+
<return-response>
25+
<set-status code="400" reason="Bad Request" />
26+
<set-header name="Content-Type" exists-action="override">
27+
<value>application/json</value>
28+
</set-header>
29+
<set-body>{"error": "Invalid cache key. Use 1-64 alphanumeric characters, hyphens, or underscores."}</set-body>
30+
</return-response>
31+
</when>
32+
</choose>
2133
<!-- Remove the entry from the APIM internal cache (no-op if absent) -->
2234
<cache-remove-value key="@((string)context.Variables["cacheKey"])" />
2335
<return-response>

samples/dynamic-cors/admin-load-cache.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@
3434
<base />
3535
<!-- Extract the cache key from the URL path parameter -->
3636
<set-variable name="cacheKey" value="@(context.Request.MatchedParameters["cacheKey"])" />
37+
<!-- Validate cache key format: alphanumeric, hyphens, underscores, max 64 characters -->
38+
<choose>
39+
<when condition="@(!System.Text.RegularExpressions.Regex.IsMatch((string)context.Variables["cacheKey"], @"^[A-Za-z0-9_\-]{1,64}$"))">
40+
<return-response>
41+
<set-status code="400" reason="Bad Request" />
42+
<set-header name="Content-Type" exists-action="override">
43+
<value>application/json</value>
44+
</set-header>
45+
<set-body>{"error": "Invalid cache key. Use 1-64 alphanumeric characters, hyphens, or underscores."}</set-body>
46+
</return-response>
47+
</when>
48+
</choose>
49+
<!-- Reject oversized request bodies (max 64 KB) -->
50+
<choose>
51+
<when condition="@(int.Parse(context.Request.Headers.GetValueOrDefault("Content-Length", "0")) > 65536)">
52+
<return-response>
53+
<set-status code="413" reason="Payload Too Large" />
54+
<set-header name="Content-Type" exists-action="override">
55+
<value>application/json</value>
56+
</set-header>
57+
<set-body>{"error": "Request body exceeds the 64 KB size limit."}</set-body>
58+
</return-response>
59+
</when>
60+
</choose>
3761
<!-- Read and validate the request body -->
3862
<set-variable name="cacheValue" value="@(context.Request.Body.As<string>(preserveContent: true))" />
3963
<choose>

0 commit comments

Comments
 (0)