← back to Commercialrealestate
just-listed: harden esc() against attribute-breakout XSS + fix CSV var shadow + Escape-to-close picker (session-close review)
692688492b6068ceae14ecd997717790bff06a7e · 2026-08-12 08:49:15 -0700 · Steve Abrams
Files touched
M public/just-listed.html
Diff
commit 692688492b6068ceae14ecd997717790bff06a7e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 08:49:15 2026 -0700
just-listed: harden esc() against attribute-breakout XSS + fix CSV var shadow + Escape-to-close picker (session-close review)
---
public/just-listed.html | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/public/just-listed.html b/public/just-listed.html
index 2b82f0f..1f21974 100644
--- a/public/just-listed.html
+++ b/public/just-listed.html
@@ -114,7 +114,7 @@ td a.f:hover{text-decoration:underline}
</div></div>
<script>
const $=s=>document.querySelector(s);
-const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<');
+const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
const money=n=>n==null||isNaN(+n)?null:'$'+Math.round(+n).toLocaleString();
let DATA=[],META={},q='',sortKey='price',sortDir=-1,ME=null;
let dens=(function(){const v=parseInt(localStorage.getItem('jlDens'),10);return(v>=2&&v<=10)?v:6;})();
@@ -250,8 +250,8 @@ $('#dens').value=dens;
function csvVal(r,c){let v=c.k==='status'?statusShort(r.status):(c.val?c.val(r):r[c.k]);return v==null?'':String(v);}
function exportCSV(){
const cols=visCols(),rows=filtered();
- const q=s=>/[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;
- const lines=[cols.map(c=>q(c.label)).join(',')].concat(rows.map(r=>cols.map(c=>q(csvVal(r,c))).join(',')));
+ const qt=s=>/[",\r\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;
+ const lines=[cols.map(c=>qt(c.label)).join(',')].concat(rows.map(r=>cols.map(c=>qt(csvVal(r,c))).join(',')));
const blob=new Blob([''+lines.join('\r\n')],{type:'text/csv;charset=utf-8'});
const url=URL.createObjectURL(blob),a=document.createElement('a'),d=new Date(),p=n=>String(n).padStart(2,'0');
a.href=url;a.download=`just-listed-${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}.csv`;
@@ -277,6 +277,7 @@ $('#colPanel').addEventListener('click',e=>{
saveCols();render();
});
document.addEventListener('click',()=>{$('#colPanel').hidden=true;$('#colBtn').setAttribute('aria-expanded','false');});
+document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!$('#colPanel').hidden){$('#colPanel').hidden=true;$('#colBtn').setAttribute('aria-expanded','false');}});
// ── auth pill (same account layer as deals-flow) ──
const api=(u,o)=>fetch(u,Object.assign({headers:{'Content-Type':'application/json'}},o||{})).then(r=>r.json());
function renderAuth(){const el=$('#auth');if(ME&&ME.email){const who=esc(ME.name||ME.username||ME.email);el.innerHTML=`<b style="color:#fff">${who}</b> · <a style="color:var(--blue);cursor:pointer">sign out</a>`;el.querySelector('a').onclick=async()=>{await fetch('/auth/logout',{method:'POST'});location.reload();};el.onclick=null;}else{el.textContent='Sign in';el.onclick=()=>location.href='/deals-flow.html';}}
← 60799a1 crcp parcel detail: safeHref protocol whitelist (XSS fix) +
·
back to Commercialrealestate
·
chore: bump v0.18.2 → v0.18.3 (session close) 59e0054 →