Skip to content

Commit ba7ea87

Browse files
grantmcdermottrandy3k
authored andcommitted
fix(plot): address JGD viewer review feedback
Apply the should-fix items from @randy3k's review of #1706: - Escape gc.col/gc.fill, font-family, text fill, and the raster href through svgEsc() in the SVG export builders; previously only text content was escaped, leaving these attributes injectable. - Encode large PNG exports to base64 in chunks (uint8ToBase64) instead of String.fromCharCode(...bytes), which overflows the call stack at large sizes/DPI. - Place the Unix domain socket in a private 0o700 mkdtemp directory and remove it on stop(), matching the IPC pipe handling from #1705. - Add a Content-Security-Policy meta tag to the JGD webview. - Set the panel icon (UriIcon 'graph') for parity with the httpgd and standard plot viewers.
1 parent 239f17a commit ba7ea87

2 files changed

Lines changed: 29 additions & 7 deletions

File tree

src/plotViewer/jgdSocketServer.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface JgdGetDimensions {
4848
export class JgdSocketServer {
4949
private server: net.Server | null = null;
5050
private socketPath: string = '';
51+
private socketDir: string = '';
5152
private sessions: Map<string, RSession> = new Map();
5253
private connectionListeners: ConnectionChangeListener[] = [];
5354
private sessionCounter = 0;
@@ -131,7 +132,11 @@ export class JgdSocketServer {
131132
this.notifyReady();
132133
});
133134
} else {
134-
this.socketPath = path.join(os.tmpdir(), `jgd-${token}.sock`);
135+
// Place the socket in a private 0o700 directory so other local
136+
// users cannot connect to it (mirrors the IPC pipe handling in #1705).
137+
this.socketDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jgd-'));
138+
try { fs.chmodSync(this.socketDir, 0o700); } catch { /* ignore */ }
139+
this.socketPath = path.join(this.socketDir, `${token}.sock`);
135140
try { fs.unlinkSync(this.socketPath); } catch { /* ignore */ }
136141

137142
this.server.listen(this.socketPath, () => {
@@ -157,6 +162,10 @@ export class JgdSocketServer {
157162
this.server?.close();
158163
if (!isWindows) {
159164
try { fs.unlinkSync(this.socketPath); } catch { /* ignore */ }
165+
if (this.socketDir) {
166+
try { fs.rmSync(this.socketDir, { recursive: true, force: true }); } catch { /* ignore */ }
167+
this.socketDir = '';
168+
}
160169
}
161170
}
162171

src/plotViewer/jgdViewer.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22
import { PlotViewer } from './types';
33
import { PlotHistory, PlotFrame } from './jgdPlotHistory';
44
import { JgdSocketServer, JgdMessage } from './jgdSocketServer';
5-
import { config } from '../util';
5+
import { config, UriIcon } from '../util';
66

77
interface MetricsRequest {
88
id: number;
@@ -413,6 +413,7 @@ export class JgdViewer implements PlotViewer {
413413
}
414414
);
415415

416+
this.panel.iconPath = new UriIcon('graph');
416417
this.panel.webview.html = this.getWebviewHtml();
417418

418419
this.panel.webview.onDidReceiveMessage((raw: WebviewMessage) => {
@@ -527,6 +528,7 @@ export class JgdViewer implements PlotViewer {
527528
<html lang="en">
528529
<head>
529530
<meta charset="UTF-8">
531+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:;">
530532
<meta name="viewport" content="width=device-width, initial-scale=1.0">
531533
<style>
532534
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -1108,7 +1110,7 @@ function handleExport(format, exportW, exportH) {
11081110
if (!blob) return;
11091111
const reader = new FileReader();
11101112
reader.onload = () => {
1111-
const base64 = btoa(String.fromCharCode(...new Uint8Array(reader.result)));
1113+
const base64 = uint8ToBase64(new Uint8Array(reader.result));
11121114
vscode.postMessage({ type: 'export_data', format: 'png', data: base64 });
11131115
};
11141116
reader.readAsArrayBuffer(blob);
@@ -1123,6 +1125,17 @@ function handleExport(format, exportW, exportH) {
11231125
11241126
function svgEsc(s) { return s.replace(/&/g,'&amp;').replace(/[<]/g,'&lt;').replace(/[>]/g,'&gt;').replace(/"/g,'&quot;'); }
11251127
1128+
// Encode bytes to base64 in chunks. A naive String.fromCharCode(...bytes)
1129+
// overflows the call stack for large exports (e.g. high DPI at large sizes).
1130+
function uint8ToBase64(bytes) {
1131+
let binary = '';
1132+
const chunk = 8192;
1133+
for (let i = 0; i < bytes.length; i += chunk) {
1134+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
1135+
}
1136+
return btoa(binary);
1137+
}
1138+
11261139
const cssFilterRe = /^(?:blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|opacity|saturate|sepia)\\s*\\([^()]*(?:\\([^)]*\\)[^()]*)*\\)(?:\\s+(?:blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|opacity|saturate|sepia)\\s*\\([^()]*(?:\\([^)]*\\)[^()]*)*\\))*$/;
11271140
function isSafeCssFilter(s) {
11281141
if (typeof s !== 'string') return false;
@@ -1137,7 +1150,7 @@ function svgClose(name) { return String.fromCharCode(60) + '/' + name + '>'; }
11371150
11381151
function svgGcStroke(gc) {
11391152
if (!gc || gc.col == null) return ' stroke="none"';
1140-
let s = ' stroke="' + gc.col + '"';
1153+
let s = ' stroke="' + svgEsc(String(gc.col)) + '"';
11411154
s += ' stroke-width="' + (gc.lwd || 1) + '"';
11421155
s += ' stroke-linecap="' + (gc.lend || 'round') + '"';
11431156
s += ' stroke-linejoin="' + (gc.ljoin || 'round') + '"';
@@ -1147,7 +1160,7 @@ function svgGcStroke(gc) {
11471160
11481161
function svgGcFill(gc) {
11491162
if (!gc || gc.fill == null) return ' fill="none"';
1150-
return ' fill="' + gc.fill + '"';
1163+
return ' fill="' + svgEsc(String(gc.fill)) + '"';
11511164
}
11521165
11531166
function svgFont(gc) {
@@ -1241,7 +1254,7 @@ function plotToSvg(plot, exportW, exportH) {
12411254
const col = (op.gc && op.gc.col != null) ? op.gc.col : 'black';
12421255
let transform = 'translate(' + op.x + ',' + op.y + ')';
12431256
if (op.rot) transform += ' rotate(' + (-op.rot) + ')';
1244-
s += svgTag('text', ' transform="' + transform + '" font-family="' + f.family + '" font-size="' + f.size + '" font-weight="' + f.weight + '" font-style="' + f.style + '" text-anchor="' + anchor + '" fill="' + col + '"') + svgEsc(op.str) + svgClose('text') + '\\n';
1257+
s += svgTag('text', ' transform="' + transform + '" font-family="' + svgEsc(String(f.family)) + '" font-size="' + f.size + '" font-weight="' + f.weight + '" font-style="' + f.style + '" text-anchor="' + anchor + '" fill="' + svgEsc(String(col)) + '"') + svgEsc(op.str) + svgClose('text') + '\\n';
12451258
break;
12461259
}
12471260
case 'raster': {
@@ -1253,7 +1266,7 @@ function plotToSvg(plot, exportW, exportH) {
12531266
const cx = dx + aw / 2, cy = dy + ah / 2;
12541267
transform = ' transform="rotate(' + (-op.rot) + ',' + cx + ',' + cy + ')"';
12551268
}
1256-
s += svgTag('image', ' x="' + dx + '" y="' + dy + '" width="' + aw + '" height="' + ah + '" href="' + op.data + '"' + transform, true) + '\\n';
1269+
s += svgTag('image', ' x="' + dx + '" y="' + dy + '" width="' + aw + '" height="' + ah + '" href="' + svgEsc(String(op.data)) + '"' + transform, true) + '\\n';
12571270
break;
12581271
}
12591272
case 'beginGroup': {

0 commit comments

Comments
 (0)