From 144b823be3a49208e60a06cb8dfd4dd8385ea143 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 12 Aug 2026 14:21:12 +0000
Subject: [PATCH 01/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?=
=?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
π‘ What
- `scanner/dashboard/console.html`μ λ²νΌμ `:hover` λ° `:disabled` μκ°μ νΌλλ°± μΆκ°
- `.close-btn` μμμ `title="Close (Esc)"` ν΄ν μΆκ°
- `tr.scan`μ λ‘λ© μν(`aria-busy="true"`)μ λν μκ°μ νΌλλ°±(opacity, pointer-events) μΆκ°
- `scanner/dashboard/index.html`μ "Clear filters" λ²νΌ μ€νμΌμ CSS ν΄λμ€(`.clear-btn`)λ‘ λΆλ¦¬νκ³ hover ν¨κ³Ό μΆκ°
π― Why
- μ€ν¬λ¦° 리λ μ¬μ©μλΏλ§ μλλΌ μκ° μ₯μ κ° μλ μΌλ° μ¬μ©μλ ARIA μν(`aria-busy`)λ λΉνμ±ν(`disabled`) μνλ₯Ό λͺ
νν μΈμ§ν μ μλλ‘ μκ°μ νΌλλ°±μ μ 곡νμ¬ μ¬μ©μ±μ ν₯μμν€κΈ° μν¨μ
λλ€.
- μΈλΌμΈ μ€νμΌμ ν΄λμ€λ‘ λΆλ¦¬νμ¬ μ½λ μ μ§λ³΄μμ±μ λμμ΅λλ€.
πΈ Before/After
(λΉμ£ΌμΌ λ³κ²½ μ¬νμΌλ‘, PR λ¦¬λ·°μ΄ νμΈμ© μ€ν¬λ¦°μ·μ΄ 첨λΆλ©λλ€.)
βΏ Accessibility
- ARIA μν(`aria-busy="true"`)μ μκ°μ UI κ°μ λκΈ°ν(parity) λ¬μ±
- λ«κΈ° λ²νΌμ λ§μ°μ€ νΈλ² μ ν΄ν(`title`)μ μ 곡νμ¬ λͺ
νν 컨ν
μ€νΈ μ λ¬
---
.jules/palette.md | 4 ++++
scanner/dashboard/console.html | 9 ++++++---
scanner/dashboard/index.html | 4 +++-
3 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/.jules/palette.md b/.jules/palette.md
index ea004e2d..63fa19c2 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -81,3 +81,7 @@
## 2026-08-12 - Skip to Content Accessibility
**Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load.
**Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline.
+
+## 2026-08-12 - Visual Parity for ARIA States
+**Learning:** Framework-less HTML elements with ARIA attributes (like `aria-busy="true"`) or semantic states (like `:disabled`) often lack native visual styling, leaving screen reader users informed but sighted users confused without explicit visual feedback.
+**Action:** Always map ARIA and disabled states to CSS visual styles (e.g., `opacity`, `cursor: not-allowed`, or `pointer-events: none`) to ensure parity between the visual UI and the accessibility tree.
diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 7ec262af..3c5590e3 100644
--- a/scanner/dashboard/console.html
+++ b/scanner/dashboard/console.html
@@ -24,7 +24,9 @@
h1{font-size:16px;margin:0;font-weight:700}
main{max-width:1000px;margin:0 auto;padding:20px}
input{font:inherit;padding:8px 10px;border:1px solid var(--border);border-radius:8px;min-width:280px}
- button{font:inherit;font-weight:600;padding:8px 14px;border:0;border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer}
+ button{font:inherit;font-weight:600;padding:8px 14px;border:0;border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer;transition:filter 0.2s, opacity 0.2s}
+ button:hover:not(:disabled){filter:brightness(0.9)}
+ button:disabled{opacity:0.6;cursor:not-allowed}
button.ghost{background:var(--surface);color:var(--primary);border:1px solid var(--border)}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin-bottom:16px}
.stats{display:flex;gap:12px;flex-wrap:wrap}
@@ -36,6 +38,7 @@
th{font-size:11px;color:var(--muted);text-transform:uppercase}
tr.scan{cursor:pointer}
tr.scan:hover{background:var(--bg)}
+ tr.scan[aria-busy="true"]{opacity:0.7;pointer-events:none}
input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.close-btn{float:right;border:0;background:transparent;font-size:16px;cursor:pointer;color:var(--muted);padding:0 4px;margin-top:-2px}
.close-btn:hover{color:var(--text)}
@@ -158,7 +161,7 @@
AppGuardrail Console
const rows=(s.findings||[]).map(f=>`| ${esc(f.severity)} |
${esc(f.rule_id)} | ${esc((f.message||"").split("\n")[0].slice(0,120))} |
${esc(f.file)}:${esc(f.line)} |
`).join("");
- d.innerHTML=`Scan #${esc(s.id)} ${esc(s.created_at)} Β· ${esc(s.repo||"β")}
+ d.innerHTML=`Scan #${esc(s.id)} ${esc(s.created_at)} Β· ${esc(s.repo||"β")}
| Severity | Rule | Finding | Location |
${rows||'| No findings. |
'}
`;
d.querySelector(".close-btn").addEventListener("click",closeDetail);
@@ -166,7 +169,7 @@ AppGuardrail Console
d.focus({preventScroll:true});
}catch(e){
if(requestId!==currentDetailRequest)return;
- d.innerHTML='Error loading details: '+esc(e.message)+'
';
+ d.innerHTML='Error loading details: '+esc(e.message)+'
';
d.querySelector(".close-btn").addEventListener("click",closeDetail);
scrollDetailIntoView(d);
d.focus({preventScroll:true});
diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index 132bc31b..46589446 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -78,6 +78,8 @@
.empty code{background:var(--bg);padding:2px 6px;border-radius:4px;color:var(--text)}
.primary-action{min-height:44px;padding:10px 16px;border:1px solid var(--primary);border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer;font:inherit;font-weight:700}
.primary-action:hover{filter:brightness(.94)}
+ .clear-btn{padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;font:inherit;color:var(--text);font-weight:500;transition:background 0.2s, filter 0.2s}
+ .clear-btn:hover{filter:brightness(0.95)}
.drop{margin-top:14px}
body.drag-active::after{content:"Drop findings.json here";position:fixed;inset:0;z-index:99;background:rgba(37,110,244,.08);border:4px dashed var(--primary);display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:var(--primary);pointer-events:none;backdrop-filter:blur(2px)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@@ -235,7 +237,7 @@ Dashboard
| Severity | Finding | File | Category | Status |
${rows||`|
No findings match the filter
-
+
|
`}
From e69faa3f519c4ade5ffc37b398467214030f01aa Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 13 Aug 2026 18:53:53 +0000
Subject: [PATCH 02/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?=
=?UTF-8?q?=EB=B6=80=20=EC=B0=B8=EC=A1=B0=20=EB=A7=81=ED=81=AC=20=EC=A0=91?=
=?UTF-8?q?=EA=B7=BC=EC=84=B1=20=ED=96=A5=EC=83=81=20(=EC=83=88=20?=
=?UTF-8?q?=ED=83=AD=20=EC=97=B4=EB=A6=BC=20=EA=B2=BD=EA=B3=A0=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
νλ©΄ νλ
κΈ° μ¬μ©μκ° μΈλΆ λ§ν¬λ₯Ό ν΄λ¦ν λ μ νμ΄ μ΄λ¦°λ€λ μ¬μ€μ μΈμ§ν μ μλλ‘ `target="_blank"` μμ±μ΄ μλ λ§ν¬μ `aria-label="... (opens in a new tab)"` μμ±μ μΆκ°νμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μκΈ°μΉ μμ 컨ν
μ€νΈ μ νμΌλ‘ μΈν νΌλμ λ°©μ§νκ³ μ κ·Όμ±μ κ°μ νμ΅λλ€.
* `scanner/dashboard/index.html` μ μ°Έμ‘° λ§ν¬ λ λλ§ λ‘μ§ μ
λ°μ΄νΈ
* `tests/test_dashboard_core.py` μ κ΄λ ¨ μ κ·Όμ± κ²μ¦ ν
μ€νΈ μΆκ°
---
.jules/palette.md | 3 +++
scanner/dashboard/index.html | 2 +-
tests/test_dashboard_core.py | 28 ++++++++++++++++++++++++++++
3 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/.jules/palette.md b/.jules/palette.md
index ea004e2d..e45d25f1 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -81,3 +81,6 @@
## 2026-08-12 - Skip to Content Accessibility
**Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load.
**Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline.
+## 2024-08-13 - Add Context Switch Warning for External Links
+**Learning:** Screen reader users need to be informed when links open in a new tab, as unexpected context switching can be disorienting. Simply adding `target="_blank"` is not enough.
+**Action:** Always add visually hidden text or an explicit `aria-label` like `(opens in a new tab)` for links with `target="_blank"`.
diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index 132bc31b..8df0a0ee 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -281,7 +281,7 @@ Dashboard
function openDetail(f){
lastFocus = document.activeElement;
const s = String(f.severity||'INFO').toUpperCase();
- const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
');
+ const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
');
const owasp = (f.owasp||[]).join(', ');
const cwe = (f.cwe||[]).join(', ');
const d = document.getElementById('detail');
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py
index 75a5809f..4380ce1c 100644
--- a/tests/test_dashboard_core.py
+++ b/tests/test_dashboard_core.py
@@ -302,3 +302,31 @@ def test_dashboard_search_escape_clears_input():
assert "e.key === 'Escape'" in html
assert "query = '';" in html
assert "render();" in html
+
+class _LinkAttributeParser(HTMLParser):
+ def __init__(self):
+ super().__init__()
+ self.links = []
+
+ def handle_starttag(self, tag, attrs):
+ if tag == "a":
+ self.links.append(dict(attrs))
+
+def test_dashboard_external_links_warn_screen_readers():
+ """External links opening in a new tab should inform screen readers."""
+ html_content = dashboard_index_path().read_text(encoding="utf-8")
+ detail_markup = re.search(
+ r"const refs = \(f\.references\|\|\[\]\)\.map\(r=>`(.*?)`\)\.join\('
'\);",
+ html_content,
+ flags=re.DOTALL,
+ )
+ assert detail_markup is not None
+
+ parser = _LinkAttributeParser()
+ parser.feed(detail_markup.group(1))
+
+ assert any(
+ attributes.get("target") == "_blank"
+ and attributes.get("aria-label") == "${esc(r)} (opens in a new tab)"
+ for attributes in parser.links
+ )
From a4009d06be1197e628327b2948849ab00828d9b2 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Fri, 14 Aug 2026 08:08:29 +0000
Subject: [PATCH 03/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?=
=?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
π‘ What
- `scanner/dashboard/console.html`μ λ²νΌμ `:hover` λ° `:disabled` μκ°μ νΌλλ°± μΆκ°
- `.close-btn` μμμ `title="Close (Esc)"` ν΄ν μΆκ°
- `tr.scan`μ λ‘λ© μν(`aria-busy="true"`)μ λν μκ°μ νΌλλ°±(opacity, pointer-events) μΆκ°
- `scanner/dashboard/index.html`μ "Clear filters" λ²νΌ μ€νμΌμ CSS ν΄λμ€(`.clear-btn`)λ‘ λΆλ¦¬νκ³ hover ν¨κ³Ό μΆκ°
π― Why
- μ€ν¬λ¦° 리λ μ¬μ©μλΏλ§ μλλΌ μκ° μ₯μ κ° μλ μΌλ° μ¬μ©μλ ARIA μν(`aria-busy`)λ λΉνμ±ν(`disabled`) μνλ₯Ό λͺ
νν μΈμ§ν μ μλλ‘ μκ°μ νΌλλ°±μ μ 곡νμ¬ μ¬μ©μ±μ ν₯μμν€κΈ° μν¨μ
λλ€.
- μΈλΌμΈ μ€νμΌμ ν΄λμ€λ‘ λΆλ¦¬νμ¬ μ½λ μ μ§λ³΄μμ±μ λμμ΅λλ€.
πΈ Before/After
(λΉμ£ΌμΌ λ³κ²½ μ¬νμΌλ‘, ν
μ€νΈ μ€ν¬λ¦½νΈλ₯Ό ν΅ν΄ κ²μ¦ μλ£λμμ΅λλ€.)
βΏ Accessibility
- ARIA μν(`aria-busy="true"`)μ μκ°μ UI κ°μ λκΈ°ν(parity) λ¬μ±
- λ«κΈ° λ²νΌμ λ§μ°μ€ νΈλ² μ ν΄ν(`title`)μ μ 곡νμ¬ λͺ
νν 컨ν
μ€νΈ μ λ¬
From 570d0736a2c23fbe1ea9b210066c2c0af79c4c62 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Fri, 14 Aug 2026 08:55:26 +0000
Subject: [PATCH 04/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?=
=?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
π‘ What
- `scanner/dashboard/console.html`μ λ²νΌμ `:hover` λ° `:disabled` μκ°μ νΌλλ°± μΆκ°
- `.close-btn` μμμ `title="Close (Esc)"` ν΄ν μΆκ°
- `tr.scan`μ λ‘λ© μν(`aria-busy="true"`)μ λν μκ°μ νΌλλ°±(opacity, pointer-events) μΆκ°
- `scanner/dashboard/index.html`μ "Clear filters" λ²νΌ μ€νμΌμ CSS ν΄λμ€(`.clear-btn`)λ‘ λΆλ¦¬νκ³ hover ν¨κ³Ό μΆκ°
- Strix CI μ€μΊ μ€ν¨ μμ : `console.html`μ `data-id` μμ±μ `s.id` λ λλ§ μ XSS λ°©μ§λ₯Ό μν `esc()` μ μ©
π― Why
- μ€ν¬λ¦° 리λ μ¬μ©μλΏλ§ μλλΌ μκ° μ₯μ κ° μλ μΌλ° μ¬μ©μλ ARIA μν(`aria-busy`)λ λΉνμ±ν(`disabled`) μνλ₯Ό λͺ
νν μΈμ§ν μ μλλ‘ μκ°μ νΌλλ°±μ μ 곡νμ¬ μ¬μ©μ±μ ν₯μμν€κΈ° μν¨μ
λλ€.
- μΈλΌμΈ μ€νμΌμ ν΄λμ€λ‘ λΆλ¦¬νμ¬ μ½λ μ μ§λ³΄μμ±μ λμμ΅λλ€.
- HTML Attribute Injection μ·¨μ½μ μ μ κ±°νμ¬ λ³΄μμ κ°ννμ΅λλ€.
πΈ Before/After
(λΉμ£ΌμΌ λ³κ²½ μ¬ν λ° XSS λ°©μ§ μ²λ¦¬, ν
μ€νΈ μ€ν¬λ¦½νΈλ₯Ό ν΅ν΄ κ²μ¦ μλ£λμμ΅λλ€.)
βΏ Accessibility
- ARIA μν(`aria-busy="true"`)μ μκ°μ UI κ°μ λκΈ°ν(parity) λ¬μ±
- λ«κΈ° λ²νΌμ λ§μ°μ€ νΈλ² μ ν΄ν(`title`)μ μ 곡νμ¬ λͺ
νν 컨ν
μ€νΈ μ λ¬
---
.jules/sentinel.md | 5 +++++
scanner/dashboard/console.html | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/.jules/sentinel.md b/.jules/sentinel.md
index 1600d432..c2ffd1ed 100644
--- a/.jules/sentinel.md
+++ b/.jules/sentinel.md
@@ -127,3 +127,8 @@
**Vulnerability:** The `/api/v1/webhook` POST endpoint in `appguardrail_core/controlplane.py` failed to validate the `url` property when accepting it into the database, leading to Stored SSRF risks. In addition, the core SSRF validation logic (`_is_safe_url`) in both the CLI and control-plane did not verify the input type (e.g. `isinstance(url, str)`). Passing non-string types (like integers) resulted in unhandled `AttributeError` exceptions inside `urllib.parse.urlparse`, which led to API 500 crashes on malicious JSON payloads.
**Learning:** Network endpoints must explicitly validate the data type of user-provided configurations prior to execution or storage. Furthermore, webhooks configured by users should always be checked for SSRF when saved, as trusting them later assumes input has already been safely validated, bypassing downstream network guardrails.
**Prevention:** Apply `_is_safe_url` checks directly upon ingestion (e.g., in `/api/v1/webhook`) and enforce type checks `if not isinstance(url, str): return False` prior to using library parsing functions like `urlparse`. Always return gracefully failing responses (like `400 Bad Request`) for unsafe URLs instead of allowing unhandled 500 server errors.
+
+## 2026-08-14 - HTML Attribute Injection Prevention
+**Vulnerability:** A Strix automated scan identified a Medium-severity XSS vulnerability (Attribute Injection) in `scanner/dashboard/console.html` where an unescaped variable `s.id` was directly interpolated into a `data-id` HTML attribute within a template literal (`data-id="${s.id}"`). This allowed an attacker to break out of the attribute quotes and inject malicious event handlers (e.g., `onmouseover`).
+**Learning:** Even custom `data-*` attributes meant for internal client-side logic require strict HTML escaping before interpolation, as breaking out of the attribute boundary enables full cross-site scripting.
+**Prevention:** Always wrap all interpolated variables inside HTML template literals with an HTML escaping function (e.g., `esc()`), regardless of whether the attribute expects executable code or just a simple string identifier.
diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 3c5590e3..9103751d 100644
--- a/scanner/dashboard/console.html
+++ b/scanner/dashboard/console.html
@@ -134,7 +134,7 @@ AppGuardrail Console
$("#trend").innerHTML=ord.map(s=>{const h=Math.round(6+((s.deploy_blocking||0)/max)*54);
const col=(s.deploy_blocking||0)>0?"var(--crit)":"var(--ok)";
return ``;}).join("")||'No scans yet.';
- $("#history tbody").innerHTML=scans.map(s=>`
+ $("#history tbody").innerHTML=scans.map(s=>`
| ${esc(s.created_at)} | ${esc(s.repo||"β")} | ${esc((s.commit||"β").slice(0,10))} |
${s.total} | ${pill(s.deploy_blocking,"var(--crit)")} | ${pill(s.new_blocking,"var(--high)")} |
`).join("")||'| No scans. POST to /api/v1/scans from CI. |
';
document.querySelectorAll("tr.scan").forEach(tr=>{
From 8964b5c352835399da707cd5ace688897808e283 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Fri, 14 Aug 2026 13:07:45 +0000
Subject: [PATCH 05/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?=
=?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
π‘ What
- `scanner/dashboard/console.html`μ λ²νΌμ `:hover` λ° `:disabled` μκ°μ νΌλλ°± μΆκ°
- `.close-btn` μμμ `title="Close (Esc)"` ν΄ν μΆκ°
- `tr.scan`μ λ‘λ© μν(`aria-busy="true"`)μ λν μκ°μ νΌλλ°±(opacity, pointer-events) μΆκ°
- `scanner/dashboard/index.html`μ "Clear filters" λ²νΌ μ€νμΌμ CSS ν΄λμ€(`.clear-btn`)λ‘ λΆλ¦¬νκ³ hover ν¨κ³Ό μΆκ°
- Strix CI μ€μΊ μ€ν¨ μμ : `console.html`μ `data-id` μμ±μ `s.id` λ λλ§ μ XSS λ°©μ§λ₯Ό μν `esc()` μ μ©
π― Why
- μ€ν¬λ¦° 리λ μ¬μ©μλΏλ§ μλλΌ μκ° μ₯μ κ° μλ μΌλ° μ¬μ©μλ ARIA μν(`aria-busy`)λ λΉνμ±ν(`disabled`) μνλ₯Ό λͺ
νν μΈμ§ν μ μλλ‘ μκ°μ νΌλλ°±μ μ 곡νμ¬ μ¬μ©μ±μ ν₯μμν€κΈ° μν¨μ
λλ€.
- μΈλΌμΈ μ€νμΌμ ν΄λμ€λ‘ λΆλ¦¬νμ¬ μ½λ μ μ§λ³΄μμ±μ λμμ΅λλ€.
- HTML Attribute Injection μ·¨μ½μ μ μ κ±°νμ¬ λ³΄μμ κ°ννμ΅λλ€.
πΈ Before/After
(λΉμ£ΌμΌ λ³κ²½ μ¬ν λ° XSS λ°©μ§ μ²λ¦¬, ν
μ€νΈ μ€ν¬λ¦½νΈλ₯Ό ν΅ν΄ κ²μ¦ μλ£λμμ΅λλ€.)
βΏ Accessibility
- ARIA μν(`aria-busy="true"`)μ μκ°μ UI κ°μ λκΈ°ν(parity) λ¬μ±
- λ«κΈ° λ²νΌμ λ§μ°μ€ νΈλ² μ ν΄ν(`title`)μ μ 곡νμ¬ λͺ
νν 컨ν
μ€νΈ μ λ¬
From 03dfb8f8fc51e2022ebf2678efd619bc076ce8cd Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Fri, 14 Aug 2026 14:41:29 +0000
Subject: [PATCH 06/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=BD=98?=
=?UTF-8?q?=EC=86=94=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20disabled=20?=
=?UTF-8?q?=EB=B0=8F=20loading=20=EC=83=81=ED=83=9C=20=EC=8B=9C=EA=B0=81?=
=?UTF-8?q?=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- #connect λ²νΌκ³Ό tr.scan μμμ :disabled λ° [aria-busy="true"] μνμ λν CSS μ€νμΌ μΆκ° (ν¬λͺ
λ κ°μ, 컀μ λ³κ²½)
- #key μ
λ ₯κ°μ΄ μμ λ #connect λ²νΌμ λΉνμ±ννλλ‘ μ΄λ²€νΈ 리μ€λ μΆκ° λ° λ‘λ© μ’
λ£ ν λΉνμ±ν μν λκΈ°ν μ²λ¦¬
---
scanner/dashboard/console.html | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 7ec262af..14fabf39 100644
--- a/scanner/dashboard/console.html
+++ b/scanner/dashboard/console.html
@@ -37,6 +37,8 @@
tr.scan{cursor:pointer}
tr.scan:hover{background:var(--bg)}
input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
+ button:disabled, button[aria-busy="true"] { opacity: 0.6; cursor: not-allowed; }
+ tr.scan[aria-busy="true"] { opacity: 0.7; cursor: wait; pointer-events: none; }
.close-btn{float:right;border:0;background:transparent;font-size:16px;cursor:pointer;color:var(--muted);padding:0 4px;margin-top:-2px}
.close-btn:hover{color:var(--text)}
.pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff}
@@ -177,10 +179,11 @@ AppGuardrail Console
}
}
}
-$("#connect").onclick=async()=>{KEY=$("#key").value.trim();if(!KEY)return;sessionStorage.setItem("ag_key",KEY);$("#key").value="";$("#connect").disabled=true;$("#connect").setAttribute("aria-busy","true");$("#connect").textContent="Connecting...";await load();$("#connect").disabled=false;$("#connect").removeAttribute("aria-busy");$("#connect").textContent="Connect";};
+$("#connect").onclick=async()=>{KEY=$("#key").value.trim();if(!KEY)return;sessionStorage.setItem("ag_key",KEY);$("#key").value="";$("#connect").disabled=true;$("#connect").setAttribute("aria-busy","true");$("#connect").textContent="Connecting...";await load();$("#connect").disabled=!$("#key").value.trim();$("#connect").removeAttribute("aria-busy");$("#connect").textContent="Connect";};
+$("#key").addEventListener("input",e=>{$("#connect").disabled=!e.target.value.trim();});
$("#key").addEventListener("keydown",e=>{if(e.key==="Enter")$("#connect").click();});
$("#logout").onclick=()=>{sessionStorage.removeItem("ag_key");location.reload();};
-if(KEY)load();
+if(KEY)load();else $("#connect").disabled=true;