mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-12 13:48:58 +02:00
fix: bind file tree names without compiling repository content
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"core.min.js": "core.6332b3c288.min.js",
|
||||
"vendor.min.js": "vendor.c40cb91530.min.js",
|
||||
"vendor.min.js": "vendor.3340ba0f11.min.js",
|
||||
"mermaid.min.js": "mermaid.f848a72d16.min.js",
|
||||
"all.min.css": "all.99ce8f3e14.min.css"
|
||||
}
|
||||
}
|
||||
|
||||
+11
-24
@@ -622,23 +622,6 @@ angular
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
// Escape a value for safe interpolation into a single-quoted
|
||||
// AngularJS expression string (e.g. ng-click="openFolder('...')")
|
||||
// that itself sits inside a double-quoted HTML attribute which is
|
||||
// later $compile()d. Backslash/quote are escaped at the Angular
|
||||
// string level; &<>" are HTML-encoded for the attribute. Without
|
||||
// this a file name like `');$emit(...)//` would break out of the
|
||||
// expression string and execute (DOM XSS, CWE-79).
|
||||
function escapeNgString(str) {
|
||||
return String(str)
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function buildSearchFilter() {
|
||||
const results = $scope.searchResults;
|
||||
if (!results || !results.length) return null;
|
||||
@@ -721,12 +704,14 @@ angular
|
||||
cssClasses.push("truncated");
|
||||
}
|
||||
|
||||
const ngPath = escapeNgString(path);
|
||||
const nodeIndex = $scope.treeNodes.length;
|
||||
$scope.treeNodes.push({ name, path, href: `/r/${encodeURIComponent($scope.repoId)}${encodePathForUrl(path)}` });
|
||||
const node = `treeNodes[${nodeIndex}]`;
|
||||
output += `<li class="${cssClasses.join(
|
||||
" "
|
||||
)}" ng-class="{active: isActive('${ngPath}'), open: ${filterSet ? "opens['" + ngPath + "'] !== false" : "opens['" + ngPath + "']"}}" title="${escapeHtml(sizeTitle)}">`;
|
||||
)}" ng-class="{active: isActive(${node}.path), open: opens[${node}.path]${filterSet ? ' !== false' : ''}}" title="${escapeHtml(sizeTitle)}">`;
|
||||
if (dir) {
|
||||
output += `<a ng-click="openFolder('${ngPath}', $event)"><span class="tree-toggle"></span><span class="tree-icon-folder"></span><span class="tree-name">${escapeHtml(name)}</span>`;
|
||||
output += `<a ng-click="openFolder(${node}.path, $event)"><span class="tree-toggle"></span><span class="tree-icon-folder"></span><span class="tree-name" ng-bind="${node}.name"></span>`;
|
||||
if (truncated) {
|
||||
output += `<span class="truncated-warning" title="{{ 'WARNINGS.folder_truncated' | translate }}"><i class="fas fa-exclamation-triangle"></i></span>`;
|
||||
}
|
||||
@@ -736,9 +721,7 @@ angular
|
||||
output += `</a>`;
|
||||
} else {
|
||||
const needsSpacer = parentPath !== "";
|
||||
output += `<a href='/r/${$scope.repoId}${encodePathForUrl(
|
||||
path
|
||||
)}'>${needsSpacer ? '<span class="tree-spacer"></span>' : ''}<span class="tree-icon-file"></span><span class="tree-name">${escapeHtml(name)}</span></a>`;
|
||||
output += `<a ng-href='{{${node}.href}}'>${needsSpacer ? '<span class="tree-spacer"></span>' : ''}<span class="tree-icon-file"></span><span class="tree-name" ng-bind="${node}.name"></span></a>`;
|
||||
}
|
||||
if (isOpen && collapsed.child) {
|
||||
const children = collapsed.child;
|
||||
@@ -757,7 +740,11 @@ angular
|
||||
return output + "</ul>";
|
||||
}
|
||||
|
||||
let renderScope = null;
|
||||
function display() {
|
||||
if (renderScope) renderScope.$destroy();
|
||||
renderScope = $scope.$new();
|
||||
$scope.treeNodes = [];
|
||||
$element.html("");
|
||||
const filterSet = $scope.searchQuery ? buildSearchFilter() : null;
|
||||
let output;
|
||||
@@ -766,7 +753,7 @@ angular
|
||||
} else {
|
||||
output = generate(toArray($scope.file).sort(sortFiles), "", filterSet);
|
||||
}
|
||||
$compile(output)($scope, (clone) => {
|
||||
$compile(output)(renderScope, (clone) => {
|
||||
$element.append(clone);
|
||||
restoreFocus();
|
||||
});
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
|
||||
const { expect } = require("chai");
|
||||
const fs = require("fs");
|
||||
const vm = require("vm");
|
||||
const path = require("path");
|
||||
const { setImmediate } = require("timers");
|
||||
const source = fs.readFileSync(path.join(__dirname, "../public/script/app.js"), "utf8");
|
||||
|
||||
function harness(date) {
|
||||
const defs = {}, routes = {}, timers = new Map();
|
||||
let timerId = 0;
|
||||
const chain = new Proxy({}, { get: (_, method) => (...args) => {
|
||||
if (["controller", "directive"].includes(method)) defs[args[0]] = args[1];
|
||||
if (method === "config") {
|
||||
const route = { when(p, opt) { routes[p] = opt; return route; }, otherwise() {} };
|
||||
args[0].at(-1)(route, { html5Mode() {} }, { useStaticFilesLoader() {}, preferredLanguage() {} });
|
||||
}
|
||||
return chain;
|
||||
} });
|
||||
const context = { angular: { module: () => chain }, console, Map, Set, Date: date || Date,
|
||||
navigator: { platform: "Linux" }, document: { location: { pathname: "/r/repo" }, addEventListener() {}, querySelector() {} },
|
||||
window: {}, Prism: { highlightAll() {} }, encodeURIComponent,
|
||||
encodePathForUrl: p => p.split("/").map(encodeURIComponent).join("/"),
|
||||
humanFileSize: x => String(x), parseGithubUrl: () => ({ owner: "owner", repo: "repo" }),
|
||||
$: () => ({ on() {}, tooltip() {} }),
|
||||
setTimeout: fn => { timers.set(++timerId, fn); return timerId; },
|
||||
clearTimeout: id => timers.delete(id), setInterval: () => 0, clearInterval() {},
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
const events = {}, watches = {};
|
||||
const scope = { $new() { return { $destroy() {} }; }, $on: (key, fn) => { (events[key] ||= []).push(fn); }, $watch: (key, fn) => { watches[key] = fn; }, $apply() {}, $applyAsync() {} };
|
||||
const requests = [];
|
||||
const http = {};
|
||||
for (const method of ["get", "post"]) http[method] = (url, body) => new Promise((resolve, reject) => requests.push({ method, url, body, resolve, reject }));
|
||||
const q = { resolve: () => Promise.resolve(), reject: e => Promise.reject(e), defer: () => { let resolve; const promise = new Promise(r => { resolve = r; }); return { promise, resolve }; } };
|
||||
return { defs, routes, scope, http, requests, q, events, watches, timers, context,
|
||||
emit: key => (events[key] || []).forEach(fn => fn()),
|
||||
flush: () => new Promise(resolve => setImmediate(resolve)),
|
||||
};
|
||||
}
|
||||
function explorer() {
|
||||
const h = harness(); h.params = { repoId: "repo", path: "README.md" };
|
||||
h.defs.exploreController.at(-1)(h.scope, h.http, { url: () => "/r/repo/README.md" }, h.params, { trustAsHtml: x => x }, h.q);
|
||||
h.navigate = path => { h.params.path = path; h.emit("$routeUpdate"); };
|
||||
return h;
|
||||
}
|
||||
|
||||
describe("frontend production regressions", function () {
|
||||
it("keeps filenames and folder paths out of compiled Angular templates", function () {
|
||||
const h = harness(); let template;
|
||||
const element = { html() {}, append() {}, 0: { addEventListener() {}, setAttribute() {} } };
|
||||
h.scope.file = [{ name: '{{constructor.constructor("window.probe=1")()}}.txt', path: "", size: 1 }];
|
||||
h.scope.$parent = {};
|
||||
h.defs.tree[0]().controller.at(-1)(element, h.scope, {}, html => { template = html; return () => {}; });
|
||||
h.watches.file(h.scope.file);
|
||||
expect(template).not.to.include("constructor.constructor");
|
||||
expect(template).to.include('ng-bind="treeNodes[0].name"');
|
||||
expect(h.scope.treeNodes[0].name).to.equal(h.scope.file[0].name);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user