Skip to content

Commit 9f6a96e

Browse files
Harden source info handoff and portal links
Signed-off-by: Yoshifumi Nakamura <nakamura@riken.jp>
1 parent 6048fbd commit 9f6a96e

10 files changed

Lines changed: 257 additions & 42 deletions

.gitlab-ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ generate_matrix:
101101
- "*.md"
102102
- "docs/**/*"
103103
- "result_server/**/*"
104+
- "requirements-result-server.txt"
104105
- "config/system_info.csv"
105106
when: never
106107
- when: always
@@ -142,6 +143,7 @@ trigger_child_pipeline:
142143
- "*.md"
143144
- "docs/**/*"
144145
- "result_server/**/*"
146+
- "requirements-result-server.txt"
145147
- "config/system_info.csv"
146148
when: never
147149
- when: always

docs/ci.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ Current skip-oriented patterns include:
230230
- `*.md`
231231
- `docs/**/*`
232232
- `result_server/**/*`
233+
- `requirements-result-server.txt`
233234
- `config/system_info.csv`
234235

235236
> Synchronization note: this list mirrors the `paths:` entries in

result_server/templates/_filter_dropdowns.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
<option value="{{ val }}" {% if current_exp == val %}selected{% endif %}>{{ val }}</option>
2020
{% endfor %}
2121
</select></label>
22+
</div>
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
{% if row.source_info and row.source_info.source_type == "git" %}
2-
<td class="code-cell"><a class="code-link" href="{{ row.source_info.repo_url }}" target="_blank" title="{{ row.source_info.repo_url }}">{{ row.code }}</a><span class="quality-badge quality-{{ row.quality.level }}" title="{{ row.quality.label }}: {{ row.quality.summary }}">{{ row.quality.label }}</span></td>
3-
{% elif row.source_info and row.source_info.source_type == "file" %}
4-
<td class="code-cell" title="{{ row.source_info.file_path }}"><span class="code-link">{{ row.code }}</span><span class="quality-badge quality-{{ row.quality.level }}" title="{{ row.quality.label }}: {{ row.quality.summary }}">{{ row.quality.label }}</span></td>
1+
{% if row.source_link and row.source_link.href %}
2+
<td class="code-cell"><a class="code-link" href="{{ row.source_link.href }}" target="_blank" rel="noopener noreferrer" title="{{ row.source_link.title }}">{{ row.code }}</a><span class="quality-badge quality-{{ row.quality.level }}" title="{{ row.quality.label }}: {{ row.quality.summary }}">{{ row.quality.label }}</span></td>
3+
{% elif row.source_link %}
4+
<td class="code-cell" title="{{ row.source_link.title }}"><span class="code-link">{{ row.code }}</span><span class="quality-badge quality-{{ row.quality.level }}" title="{{ row.quality.label }}: {{ row.quality.summary }}">{{ row.quality.label }}</span></td>
55
{% else %}
66
<td class="code-cell" title="{{ row.code }}"><span class="code-link">{{ row.code }}</span><span class="quality-badge quality-{{ row.quality.level }}" title="{{ row.quality.label }}: {{ row.quality.summary }}">{{ row.quality.label }}</span></td>
77
{% endif %}

result_server/tests/test_portal_list_templates.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,30 @@ def test_pagination_template_urlencodes_filters_without_inline_javascript():
173173
assert 'code" onclick="alert(1)' not in html
174174

175175

176+
def test_code_cell_adds_noopener_to_source_links():
177+
app = build_portal_shell_app(
178+
templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"),
179+
)
180+
with app.test_request_context("/results"):
181+
from flask import render_template
182+
183+
html = render_template(
184+
"_results_table_cell_code.html",
185+
row={
186+
"code": "qws",
187+
"source_link": {
188+
"href": "https://example.invalid/repo.git",
189+
"title": "https://example.invalid/repo.git",
190+
},
191+
"quality": {"level": "ready", "label": "Ready", "summary": "ok"},
192+
},
193+
)
194+
195+
assert 'target="_blank"' in html
196+
assert 'rel="noopener noreferrer"' in html
197+
assert 'href="https://example.invalid/repo.git"' in html
198+
199+
176200
def test_estimated_results_template_renders_table_note():
177201
app = build_portal_shell_app(
178202
templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"),
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Static checks for source_info handoff scripts."""
2+
3+
from pathlib import Path
4+
5+
6+
REPO_ROOT = Path(__file__).resolve().parents[2]
7+
8+
9+
def test_result_script_does_not_source_source_info_env():
10+
result_script = (REPO_ROOT / "scripts" / "result.sh").read_text(encoding="utf-8")
11+
12+
assert ". results/source_info.env" not in result_script
13+
assert "source results/source_info.env" not in result_script
14+
assert "build_source_info_block" in result_script
15+
assert "jq -n" in result_script
16+
17+
18+
def test_bk_fetch_source_writes_encoded_source_info_values():
19+
bk_functions = (REPO_ROOT / "scripts" / "bk_functions.sh").read_text(encoding="utf-8")
20+
21+
assert "BK_SOURCE_INFO_FORMAT=base64-v1" in bk_functions
22+
assert "BK_REPO_URL_B64" in bk_functions
23+
assert "BK_FILE_PATH_B64" in bk_functions
24+
assert 'export BK_REPO_URL="$BK_REPO_URL"' not in bk_functions
25+
assert 'export BK_FILE_PATH="$BK_FILE_PATH"' not in bk_functions

result_server/tests/test_source_info_properties.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
1212

1313
from test_support import install_portal_test_stubs
14+
from utils.result_table_rows import _build_source_link
1415

1516
install_portal_test_stubs()
1617

@@ -77,3 +78,51 @@ def test_file_source_info_structure_valid_with_md5sum(self, md5sum):
7778
assert "file_path" in source_info
7879
assert "md5sum" in source_info
7980
assert MD5SUM_PATTERN.match(source_info["md5sum"]) is not None
81+
82+
83+
def test_git_source_link_allows_http_urls_only():
84+
source_info = {
85+
"source_type": "git",
86+
"repo_url": "https://github.com/example/repo.git",
87+
}
88+
89+
link = _build_source_link(source_info)
90+
91+
assert link["href"] == "https://github.com/example/repo.git"
92+
assert link["title"] == "https://github.com/example/repo.git"
93+
94+
95+
def test_git_source_link_rejects_javascript_urls():
96+
source_info = {
97+
"source_type": "git",
98+
"repo_url": "javascript:alert(1)",
99+
}
100+
101+
link = _build_source_link(source_info)
102+
103+
assert link["href"] is None
104+
assert link["title"] == "Repository URL is not linkable"
105+
106+
107+
def test_git_source_link_rejects_ambiguous_urls():
108+
source_info = {
109+
"source_type": "git",
110+
"repo_url": "https://example.invalid\\@evil.invalid/repo.git",
111+
}
112+
113+
link = _build_source_link(source_info)
114+
115+
assert link["href"] is None
116+
assert link["title"] == "Repository URL is not linkable"
117+
118+
119+
def test_file_source_link_uses_basename_only():
120+
source_info = {
121+
"source_type": "file",
122+
"file_path": "/sensitive/path/archive.tar.gz",
123+
}
124+
125+
link = _build_source_link(source_info)
126+
127+
assert link["href"] is None
128+
assert link["title"] == "archive.tar.gz"

result_server/utils/result_table_rows.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
from urllib.parse import urlsplit
3+
14
from flask import url_for
25

36
from utils.result_records import (
@@ -13,6 +16,7 @@ def build_result_table_row(json_filename, result_data, padata_filenames):
1316
matched_padata = _find_matching_padata_archive(json_filename, result_data, padata_filenames)
1417
pipeline_timing = result_data.get("pipeline_timing", {})
1518
source_info = result_data.get("source_info")
19+
source_link = _build_source_link(source_info)
1620
profile_data = result_data.get("profile_data")
1721

1822
ci_trigger = result_data.get("ci_trigger", "-") or "-"
@@ -47,6 +51,7 @@ def build_result_table_row(json_filename, result_data, padata_filenames):
4751
"run_job": result_data.get("run_job", "-") or "-",
4852
"pipeline_id": pipeline_id,
4953
"source_info": source_info,
54+
"source_link": source_link,
5055
"source_hash": _format_source_hash(source_info),
5156
"quality": summarize_result_quality(result_data),
5257
"profile_data": profile_data,
@@ -97,6 +102,36 @@ def _format_source_hash(source_info):
97102
return "-"
98103

99104

105+
def _build_source_link(source_info):
106+
if not isinstance(source_info, dict):
107+
return None
108+
109+
source_type = source_info.get("source_type")
110+
if source_type == "git":
111+
repo_url = str(source_info.get("repo_url") or "").strip()
112+
parsed = urlsplit(repo_url)
113+
has_unsafe_chars = any(ch.isspace() for ch in repo_url) or "\\" in repo_url
114+
if parsed.scheme in {"http", "https"} and parsed.netloc and not has_unsafe_chars:
115+
return {
116+
"href": repo_url,
117+
"title": repo_url,
118+
}
119+
return {
120+
"href": None,
121+
"title": "Repository URL is not linkable",
122+
}
123+
124+
if source_type == "file":
125+
file_path = str(source_info.get("file_path") or "").strip()
126+
filename = os.path.basename(file_path) if file_path else "source archive"
127+
return {
128+
"href": None,
129+
"title": filename or "source archive",
130+
}
131+
132+
return None
133+
134+
100135
def _format_profile_summary(profile_data):
101136
if not isinstance(profile_data, dict) or not profile_data:
102137
return "-"

scripts/bk_functions.sh

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,43 @@ bk_emit_overlap() {
12501250
bk_emit_section "overlap:${_bk_ovl_sections}" "$_bk_ovl_time" "$_bk_ovl_package" "$_bk_ovl_artifact" --type overlap --members "$_bk_ovl_sections"
12511251
}
12521252

1253+
bk_base64_encode_value() {
1254+
if command -v base64 >/dev/null 2>&1; then
1255+
printf '%s' "$1" | base64 | tr -d '\r\n'
1256+
return 0
1257+
fi
1258+
if command -v openssl >/dev/null 2>&1; then
1259+
printf '%s' "$1" | openssl base64 -A | tr -d '\r\n'
1260+
return 0
1261+
fi
1262+
echo "bk_base64_encode_value: neither base64 nor openssl found" >&2
1263+
return 1
1264+
}
1265+
1266+
bk_write_source_info_env() {
1267+
_bk_source_type="$1"
1268+
_bk_repo_url="${2:-}"
1269+
_bk_branch="${3:-}"
1270+
_bk_commit_hash="${4:-}"
1271+
_bk_file_path="${5:-}"
1272+
_bk_md5sum="${6:-}"
1273+
1274+
if ! command -v base64 >/dev/null 2>&1 && ! command -v openssl >/dev/null 2>&1; then
1275+
echo "bk_write_source_info_env: neither base64 nor openssl found" >&2
1276+
return 1
1277+
fi
1278+
1279+
{
1280+
printf 'BK_SOURCE_INFO_FORMAT=base64-v1\n'
1281+
printf 'BK_SOURCE_TYPE_B64=%s\n' "$(bk_base64_encode_value "$_bk_source_type")"
1282+
printf 'BK_REPO_URL_B64=%s\n' "$(bk_base64_encode_value "$_bk_repo_url")"
1283+
printf 'BK_BRANCH_B64=%s\n' "$(bk_base64_encode_value "$_bk_branch")"
1284+
printf 'BK_COMMIT_HASH_B64=%s\n' "$(bk_base64_encode_value "$_bk_commit_hash")"
1285+
printf 'BK_FILE_PATH_B64=%s\n' "$(bk_base64_encode_value "$_bk_file_path")"
1286+
printf 'BK_MD5SUM_B64=%s\n' "$(bk_base64_encode_value "$_bk_md5sum")"
1287+
} > results/source_info.env
1288+
}
1289+
12531290
# bk_fetch_source - Fetch source code and collect metadata.
12541291
#
12551292
# Usage:
@@ -1273,7 +1310,7 @@ bk_emit_overlap() {
12731310
# BK_MD5SUM - (file) Full 32-char md5sum
12741311
#
12751312
# Side effects:
1276-
# Writes results/source_info.env in export format
1313+
# Writes results/source_info.env as data, not executable shell
12771314
#
12781315
# Returns:
12791316
# 0 - success
@@ -1331,13 +1368,7 @@ bk_fetch_source() {
13311368

13321369
export BK_BRANCH BK_COMMIT_HASH
13331370

1334-
# Write results/source_info.env
1335-
cat > results/source_info.env <<EOF
1336-
export BK_SOURCE_TYPE="git"
1337-
export BK_REPO_URL="$BK_REPO_URL"
1338-
export BK_BRANCH="$BK_BRANCH"
1339-
export BK_COMMIT_HASH="$BK_COMMIT_HASH"
1340-
EOF
1371+
bk_write_source_info_env "git" "$BK_REPO_URL" "$BK_BRANCH" "$BK_COMMIT_HASH"
13411372

13421373
else
13431374
# --- File archive path ---
@@ -1380,12 +1411,7 @@ EOF
13801411
fi
13811412
fi
13821413

1383-
# Write results/source_info.env
1384-
cat > results/source_info.env <<EOF
1385-
export BK_SOURCE_TYPE="file"
1386-
export BK_FILE_PATH="$BK_FILE_PATH"
1387-
export BK_MD5SUM="$BK_MD5SUM"
1388-
EOF
1414+
bk_write_source_info_env "file" "" "" "" "$BK_FILE_PATH" "$BK_MD5SUM"
13891415

13901416
fi
13911417

0 commit comments

Comments
 (0)