Add TypeScript web demo using compiled Closure bundle#2
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the WebChannel multi-language client directory, featuring a JavaScript/TypeScript client based on the Google Closure Library, complete with type definitions, unit tests, and a local web demo. Feedback on the demo code highlights a high-severity directory traversal vulnerability in the Node.js server (server.js) and a connection leak along with potential runtime TypeErrors in the client script (demo.ts). Code suggestions are provided to secure the server path resolution and properly manage active channel connections.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Prevent directory traversal attacks | ||
| let safeUrl = req.url.split('?')[0]; | ||
| let filePath = path.join(__dirname, safeUrl === '/' ? 'index.html' : safeUrl); | ||
|
|
||
| if (!filePath.startsWith(__dirname)) { | ||
| res.writeHead(403); | ||
| res.end('Forbidden'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The path resolution logic is vulnerable to directory traversal attacks. Checking if filePath starts with __dirname is insufficient because a path like /app/webchannel/js/demo-sibling/file.js also starts with /app/webchannel/js/demo if __dirname is /app/webchannel/js/demo. Additionally, the URL should be decoded using decodeURIComponent before resolving the path to handle URL-encoded traversal sequences correctly.
To fix this, decode the URL and use path.relative to verify that the resolved path is strictly within the target directory.
| // Prevent directory traversal attacks | |
| let safeUrl = req.url.split('?')[0]; | |
| let filePath = path.join(__dirname, safeUrl === '/' ? 'index.html' : safeUrl); | |
| if (!filePath.startsWith(__dirname)) { | |
| res.writeHead(403); | |
| res.end('Forbidden'); | |
| return; | |
| } | |
| // Prevent directory traversal attacks and decode URL components | |
| const decodedUrl = decodeURIComponent(req.url.split('?')[0]); | |
| const filePath = path.join(__dirname, decodedUrl === '/' ? 'index.html' : decodedUrl); | |
| const relative = path.relative(__dirname, filePath); | |
| if (relative.startsWith('..') || path.isAbsolute(relative)) { | |
| res.writeHead(403); | |
| res.end('Forbidden'); | |
| return; | |
| } |
|
|
||
| sendButton.addEventListener('click', () => { | ||
| const text = messageInput.value || 'Hello from WebChannel TS Demo!'; | ||
| log(`>>> Sending request with message: "${text}"`); | ||
|
|
||
| const options = { | ||
| supportsCrossDomainXhr: true, | ||
| httpSessionIdParam: 'gsessionid' | ||
| }; | ||
|
|
||
| const channel = channelFactory.createWebChannel(url, options); | ||
|
|
||
| channel.listen(WebChannel.EventType.OPEN, () => { | ||
| log('>>> WebChannel opened!'); | ||
|
|
||
| const payload = { | ||
| message: text, | ||
| num_messages: 5, | ||
| message_interval: 1000 | ||
| }; | ||
|
|
||
| channel.send(payload); | ||
| log(`>>> Sent request payload: ${JSON.stringify(payload)}`); | ||
| }); | ||
|
|
||
| channel.listen(WebChannel.EventType.MESSAGE, (event: any) => { | ||
| const inbound = event.data; | ||
| log(`<<< Received message event. Raw data: ${JSON.stringify(inbound)}`); | ||
|
|
||
| const result = Array.isArray(inbound) ? inbound[0] : inbound; | ||
| if (result) { | ||
| if (result.error) { | ||
| log(`<<< ERROR from server: ${result.error.message}`); | ||
| } else if (result.message) { | ||
| log(`<<< Echo response message: "${result.message}"`); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| channel.listen(WebChannel.EventType.ERROR, (error: any) => { | ||
| log(`<<< WebChannel error: ${JSON.stringify(error)}`); | ||
| }); | ||
|
|
||
| channel.listen(WebChannel.EventType.CLOSE, () => { | ||
| log('<<< WebChannel closed!'); | ||
| }); | ||
|
|
||
| channel.open(); | ||
| }); |
There was a problem hiding this comment.
Every time the 'Send Streaming Echo' button is clicked, a new WebChannel connection is created and opened without closing the previous one. This leads to connection leaks and duplicate event listeners logging to the same UI element. Additionally, accessing sendButton and messageInput without null checks can throw runtime TypeErrors if the DOM elements are missing.
Use optional chaining and track the active channel to close any existing connection before opening a new one.
let activeChannel: any = null;
sendButton?.addEventListener('click', () => {
if (activeChannel) {
log('>>> Closing previous active WebChannel connection...');
activeChannel.close();
}
const text = messageInput?.value || 'Hello from WebChannel TS Demo!';
log('>>> Sending request with message: "' + text + '"');
const options = {
supportsCrossDomainXhr: true,
httpSessionIdParam: 'gsessionid'
};
const channel = channelFactory.createWebChannel(url, options);
activeChannel = channel;
channel.listen(WebChannel.EventType.OPEN, () => {
log('>>> WebChannel opened!');
const payload = {
message: text,
num_messages: 5,
message_interval: 1000
};
channel.send(payload);
log('>>> Sent request payload: ' + JSON.stringify(payload));
});
channel.listen(WebChannel.EventType.MESSAGE, (event: any) => {
const inbound = event.data;
log('<<< Received message event. Raw data: ' + JSON.stringify(inbound));
const result = Array.isArray(inbound) ? inbound[0] : inbound;
if (result) {
if (result.error) {
log('<<< ERROR from server: ' + result.error.message);
} else if (result.message) {
log('<<< Echo response message: "' + result.message + '"');
}
}
});
channel.listen(WebChannel.EventType.ERROR, (error: any) => {
log('<<< WebChannel error: ' + JSON.stringify(error));
if (activeChannel === channel) {
activeChannel = null;
}
});
channel.listen(WebChannel.EventType.CLOSE, () => {
log('<<< WebChannel closed!');
if (activeChannel === channel) {
activeChannel = null;
}
});
channel.open();
});90476a2 to
d41faaf
Compare
|
I have resolved the review comments:
All changes have been successfully tested and force-pushed to the |
d41faaf to
6b4fb2f
Compare
Integrated the newly built ES2022 CJS module blob into the local TS web demo, verifying end-to-end streaming against the MessageGenerator.