Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.048"
VERSION = "0.261.049"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
16 changes: 13 additions & 3 deletions application/single_app/static/js/chat/chat-inline-maps.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { escapeHtml } from "./chat-utils.js";

const AZURE_MAPS_RENDER_TYPE = "azure_maps_openlayers";
const AZURE_MAPS_TILE_PROXY_PATH = "/api/azure-maps/tile";

function toFiniteNumber(value) {
const numericValue = Number(value);
Expand Down Expand Up @@ -56,14 +57,22 @@
return null;
}

// Tiles are requested with the user's session, so they only ever come from SimpleChat's tile proxy.
function isTileProxyTemplate(template) {
const value = typeof template === "string" ? template.trim() : "";
return value.startsWith(`${AZURE_MAPS_TILE_PROXY_PATH}?`)
&& !/[\s"'<>\\]/.test(value)
&& ["{z}", "{x}", "{y}"].every((placeholder) => value.includes(placeholder));
}

function isAzureMapsVisualization(result) {
return Boolean(
result
&& result.success !== false
&& result.render_type === AZURE_MAPS_RENDER_TYPE
&& result.map_payload
&& typeof result.map_payload === "object"
&& result.map_payload.tile_url_template
&& isTileProxyTemplate(result.map_payload.tile_url_template)
);
}

Expand Down Expand Up @@ -389,8 +398,9 @@

const tileLayer = new olRef.layer.Tile({
source: new olRef.source.XYZ({
url: payload.tile_url_template,
attributions: payload.tile_attribution || "",
url: payload.tile_url_template.trim(),
// OpenLayers renders attributions as HTML, and this text comes from a tool result.

Check warning on line 402 in application/single_app/static/js/chat/chat-inline-maps.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
attributions: escapeHtml(payload.tile_attribution || ""),

Check warning on line 403 in application/single_app/static/js/chat/chat-inline-maps.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
crossOrigin: "anonymous",
maxZoom: payload.view?.max_zoom || 15,
}),
Expand Down
62 changes: 62 additions & 0 deletions docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Inline Map Attribution Text Fix

Fixed/Implemented in version: **0.261.049**

## Issue Description

The chat's inline map card passed a tool result's `map_payload.tile_attribution` straight to

Check warning on line 7 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 7 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
OpenLayers, which renders attribution strings as HTML. The card also drew any tool result shaped

Check warning on line 8 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
like a map, whatever its `tile_url_template` named. A result from a tool other than the Azure

Check warning on line 9 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
Maps action could therefore place markup in the map's attribution, or make the browser load map
tiles from another host.

## Root Cause Analysis

- `initializeOpenLayersMap` set `attributions: payload.tile_attribution`, and OpenLayers'
Attribution control writes each attribution string into the page as HTML.
- `isAzureMapsVisualization` only checked that `tile_url_template` was present, not that it
pointed at SimpleChat's tile proxy.
- The Azure Maps action itself always returns a fixed attribution and a proxied tile template,
so maps from that action were never affected.

## Version Implemented

- **0.261.049**
- Application version updated in `application/single_app/config.py` to `0.261.049`.

## Files Modified

- `application/single_app/static/js/chat/chat-inline-maps.js`
- `application/single_app/config.py`
- `functional_tests/test_chat_inline_map_attribution_text.py`
- `docs/explanation/release_notes.md`

## Code Changes Summary

- The attribution is passed through the existing `escapeHtml` helper before OpenLayers receives

Check warning on line 36 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
it, so it is shown as text.
- New `isTileProxyTemplate` accepts only templates that start with `/api/azure-maps/tile?`,
contain `{z}`, `{x}` and `{y}`, and have no whitespace, quotes, angle brackets or backslashes.
`isAzureMapsVisualization` requires it, so a result naming any other tile source is not drawn.

## Testing Approach

- `functional_tests/test_chat_inline_map_attribution_text.py` checks that the attribution is
escaped and that the map check requires the proxy template. It then runs the real
`isTileProxyTemplate` function in Node against proxy, foreign, protocol-relative, incomplete and
quoted templates.
- `ui_tests/test_chat_inline_azure_maps_rendering.py` already uses a proxied template, so the
existing rendering coverage still applies.

## Impact Analysis

- Maps from the Azure Maps action render exactly as before; the escaped attribution reads the
same.
- A tool result that only resembles a map, with a tile template outside the proxy, is no longer

Check warning on line 55 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
drawn as a map. Its citation is still available through the normal citation controls.

Check warning on line 56 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

## Validation

- Before: attribution text from a tool result reached the page as HTML, and tiles could load from

Check warning on line 60 in docs/explanation/fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
any template.
- After: the attribution is text, and tiles only load through `/api/azure-maps/tile`.
9 changes: 9 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes).

### **(v0.261.049)**

#### Bug Fixes

* **Inline Map Attribution Shown as Text and Tiles Kept on the Tile Proxy**
* Inline maps in the chat now show a tool result's tile attribution as plain text. OpenLayers renders attribution strings as HTML, so the text is escaped first.
* A tool result is drawn as a map only when its tile template points at SimpleChat's Azure Maps tile proxy (`/api/azure-maps/tile`). Maps from the Azure Maps action are unchanged.
* (Ref: `chat-inline-maps.js`, [Inline Map Attribution Text Fix](fixes/INLINE_MAP_ATTRIBUTION_TEXT_FIX.md))

### **(v0.261.048)**

#### Bug Fixes
Expand Down
111 changes: 111 additions & 0 deletions functional_tests/test_chat_inline_map_attribution_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# test_chat_inline_map_attribution_text.py
"""
Functional test for the inline map attribution text fix.
Version: 0.261.049
Implemented in: 0.261.049

This test ensures that the chat's inline map card shows a tool result's tile attribution as
text rather than HTML, and draws a tool result as a map only when its tile template points at
SimpleChat's Azure Maps tile proxy.
"""

import json
import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
INLINE_MAPS_JS = ROOT / "application" / "single_app" / "static" / "js" / "chat" / "chat-inline-maps.js"

sys.path.insert(0, str(ROOT / "functional_tests"))

from test_support.versioning import assert_app_version_at_least # noqa: E402

IMPLEMENTED_IN = "0.261.049"


def _source():
return INLINE_MAPS_JS.read_text(encoding="utf-8")


def test_attribution_is_escaped_before_openlayers_renders_it():
"""OpenLayers writes attributions as HTML, so the tool result's text is escaped first."""
print("Testing inline map attribution escaping...")
source = _source()

assert 'import { escapeHtml } from "./chat-utils.js";' in source
assert 'attributions: escapeHtml(payload.tile_attribution || ""),' in source
assert "attributions: payload.tile_attribution" not in source
print("Attribution escaping passed!")


def test_maps_require_the_tile_proxy_template():
"""A tool result is drawn as a map only when its tiles come through the tile proxy."""
print("Testing the tile template requirement...")
source = _source()

assert 'const AZURE_MAPS_TILE_PROXY_PATH = "/api/azure-maps/tile";' in source
assert "&& isTileProxyTemplate(result.map_payload.tile_url_template)" in source
assert "&& result.map_payload.tile_url_template\n" not in source
print("Tile template requirement passed!")


def test_tile_proxy_template_check_behaves():
"""Run the real isTileProxyTemplate function in Node against accepted and refused templates."""
print("Testing isTileProxyTemplate in Node...")
source = _source()

match = re.search(r"function isTileProxyTemplate\(template\) \{.*?\n\}\n", source, re.S)
assert match, "isTileProxyTemplate was not found"

proxy = (
"/api/azure-maps/tile?token=abc&api-version=2024-04-01&tilesetId=microsoft.base.road"
"&zoom={z}&x={x}&y={y}&tileSize=256"
)
cases = {
proxy: True,
f" {proxy} ": True,
"https://tiles.example.test/{z}/{x}/{y}.png": False,
"//tiles.example.test/api/azure-maps/tile?{z}{x}{y}": False,
"/api/azure-maps/tiles?{z}{x}{y}": False,
"/api/azure-maps/tile?zoom={z}&x={x}": False,
'/api/azure-maps/tile?token="><img src=x>&{z}{x}{y}': False,
"": False,
}
script = (
'const AZURE_MAPS_TILE_PROXY_PATH = "/api/azure-maps/tile";\n'
+ match.group(0)
+ f"const cases = {json.dumps(list(cases.items()))};\n"
+ "const wrong = cases.filter(([value, expected]) => isTileProxyTemplate(value) !== expected);\n"
+ "for (const value of [undefined, null, 42, {}]) { if (isTileProxyTemplate(value)) wrong.push([String(value), false]); }\n"
+ "if (wrong.length) { console.error(JSON.stringify(wrong)); process.exit(1); }\n"
+ "console.log(`${cases.length + 4} template checks passed`);\n"
)
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, f"isTileProxyTemplate returned the wrong answer for: {result.stderr}"
print(f" {result.stdout.strip()}")
print("isTileProxyTemplate passed!")


def test_version_was_incremented():
assert_app_version_at_least(IMPLEMENTED_IN)


if __name__ == "__main__":
tests = [
test_attribution_is_escaped_before_openlayers_renders_it,
test_maps_require_the_tile_proxy_template,
test_tile_proxy_template_check_behaves,
test_version_was_incremented,
]
failures = 0
for test in tests:
try:
test()
except Exception as exc: # noqa: BLE001 - report every failing check
failures += 1
print(f"FAILED {test.__name__}: {exc}")
print(f"\n{len(tests) - failures}/{len(tests)} tests passed")
sys.exit(1 if failures else 0)
Loading