Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions lib/deploy/hosting/validateConfigGlobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const _ = require("lodash");

/**
* validateConfigGlobs takes a hosting config from firebase.json and scans
* for known malformed globs, returning an array of warning strings
*/
module.exports = function(config) {
if (!config) {
return;
}
warnings = [];

// rewrites
if (_.isArray(config.rewrites)) {
config.rewrites.map(function(rewrite) {
warnings = _.union(warnings, _validateGlob(rewrite.source));
if (rewrite.destination && !rewrite.function) {
warnings = _.union(warnings, validateGlob(rewrite.destination));
}
});
}

// redirects
if (_.isArray(config.redirects)) {
config.redirects.map(function(redirect) {
warnings = _.union(warnings, _validateGlob(redirect.source));
if (redirect.destination) {
warnings = _.union(warnings, _validateGlob(redirect.destination));
}
});
}

// headers
if (_.isArray(config.headers)) {
config.headers.map(function(header) {
warnings = _.union(warnings, _validateGlob(header.source));
if (header.destination) {
warnings = _.union(warnings, _validateGlob(header.destination));
}
});
}

return warnings;
};

function _validateGlob(glob) {
warnings = [];
function _warn(message) {
warnings.push("Configured glob [" + glob + "] " + message);
}

// multiple slashes
if (glob.includes("//")) {
_warn(
"contains multiple slash delimiters '//'; note that these will only match exactly the same number of slashes in a path"
);
}

// breached recursion limit
if ((glob.match(/\*\*/g) || []).length > 3) {
_warn(
"contains 3+ chained ** wildcards, which may result in degraded performance or unexpected behavior"
);
}

// breached length limit
if (glob.length > 500) {
_warn("is longer than 500 characters and will be ignored by the Firebase hosting backend");
}

// malformed redirect captures
// - containing RFC1738 unsafe/reserved characters in their name
// - contains special pathToRegexp characters
// - beginning in the middle of a segment
for (var segment of glob.split("/")) {
if (segment[0] == ":") {
if (segment.includes("?") || segment.includes("+") || segment.includes("*")) {
_warn(
"contains a capture redirect " +
segment +
" with a character (?+*) with special meaning in Express paths; this will break if you are expecting literal matching"
);
} else if (!/^:[^()\|\\\^~\[\];\/\?:@\-&]*\??$/.test(segment)) {
_warn(
"contains a capture redirect " +
segment +
" with a RFC1738 unsafe or reserved character; this glob will not be evaluated"
);
}
} else if (segment.includes(":")) {
_warn(
"contains an illegal capture redirect " +
segment +
" beginning in the middle of the path segment"
);
}
}

// malformed extglobs/classes
// - unclosed (), []
// - '/' inside extglob
var stackLevel = 0;
var inClass = false;
for (var c of glob) {
switch (c) {
case "[":
inClass = true;
break;
case "]":
if (!inClass) {
_warn("contains an character class close ']' without corresponding '['");
}
inClass = false;
break;
case "(":
if (!inClass) {
stackLevel++;
}
break;
case ")":
if (!inClass) {
if (stackLevel == 0) {
_warn("contains an extglob close ')' without corresponding '('");
} else {
stackLevel--;
}
}
break;
case "/":
if (stackLevel > 0) {
_warn(
"contains '/' inside an extglob '()'. This behavior is undefined and will not match as intended."
);
}
break;
}
}
if (stackLevel > 0) {
_warn("contains an unclosed extglob paren '('");
}
if (inClass) {
_warn("contains an unclosed character class '['");
}

// numerical brace expansion {1..10}
if (/{\d+\.\.\d+}/.test(glob)) {
_warn("contains an unsupported numerical range brace expansion {x..y}");
}

return warnings;
}
6 changes: 6 additions & 0 deletions src/deploy/hosting/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const clc = require("cli-color");

const api = require("../../api");
const convertConfig = require("./convertConfig");
const validateConfigGlobs = require("./validateConfigGlobs");
const deploymentTool = require("../../deploymentTool");
const FirebaseError = require("../../error");
const fsutils = require("../../fsutils");
Expand Down Expand Up @@ -63,6 +64,11 @@ module.exports = function(context, options) {
);
}

warnings = validateConfigGlobs(cfg);
for (var w of warnings) {
utils.logWarning(w);
}

versionCreates.push(
api
.request("POST", "/v1beta1/sites/" + deploy.site + "/versions", {
Expand Down
88 changes: 88 additions & 0 deletions test/lib/validateConfigGlobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use strict";

var chai = require("chai");
var expect = chai.expect;
var validateConfigGlobs = require("../../lib/deploy/hosting/validateConfigGlobs");

function warningShown(glob) {
var cfg = { rewrites: [{ source: glob }] };
return validateConfigGlobs(cfg).length > 0;
}

describe("glob validation", function() {
it("should not fire on a normal glob", function() {
expect(warningShown("/foo/bar")).to.be.false;
});

it("should not fire on the empty string", function() {
expect(warningShown("")).to.be.false;
});

it("should detect multiple slashes", function() {
expect(warningShown("foo//bar")).to.be.true;
});

it("should detect exploding globstars", function() {
expect(warningShown("/a/**/b/**/c/**/d/**")).to.be.true;
expect(warningShown("/**/**/**/**")).to.be.true;
});

it("should detect overly long globs", function() {
// eslint-disable-next-line
expect(
warningShown(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)
).to.be.true;
});

it("should interpret length limits in terms of codepoints instead of bytes", function() {
// eslint-disable-next-line
expect(
warningShown(
"ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ"
)
).to.be.false;
});

it("should allow most L-category UTF-8 characters", function() {
expect(warningShown("utf8/あ")).to.be.false;
expect(warningShown("capture/:あ")).to.be.false;
expect(warningShown("astral/🤔")).to.be.false;
});

it("should detect malformed extglobs", function() {
expect(warningShown("@(abc)")).to.be.false;
expect(warningShown("@(abc")).to.be.true;
expect(warningShown("@(abc!(def)")).to.be.true;
expect(warningShown("abc)")).to.be.true;
expect(warningShown("@(abc[)]")).to.be.true;
});

it("should detect malformed literal classes", function() {
expect(warningShown("[abc]")).to.be.false;
expect(warningShown("[abc")).to.be.true;
expect(warningShown("[abc@(])")).to.be.true;
expect(warningShown("abc]")).to.be.true;
});

it("should detect '/' inside of an extglob", function() {
expect(warningShown("!(foo/bar)")).to.be.true;
});

it("should detect bash numerical sequence expansions", function() {
expect(warningShown("{0..99}")).to.be.true;
});

it("should detect captures with illegal characters", function() {
expect(warningShown("/:abc@def")).to.be.true;
});

it("should detect captures with special characters", function() {
expect(warningShown("/:abc?q=d")).to.be.true;
});

it("should detect captures beginning in the middle of a segment", function() {
expect(warningShown("/abc:def")).to.be.true;
});
});