Skip to content
Merged
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
27 changes: 21 additions & 6 deletions tools/dev-server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class DevServer {
port: this.port,
notify: false,
ghostMode: false,
server: true,
server: false,
middleware: (req, res) => this._bazelMiddleware(req, res),
};

Expand Down Expand Up @@ -59,10 +59,21 @@ export class DevServer {
*/
private _bazelMiddleware(req: http.IncomingMessage, res: http.ServerResponse) {
if (!req.url) {
res.end('No url specified. Error');
res.statusCode = 500;
res.end('Error: No url specified');
return;
}

// Detect if the url escapes the server's root path
for (const rootPath of this._rootPaths) {
const absoluteRootPath = path.resolve(rootPath);
const absoluteJoinedPath = path.resolve(path.posix.join(rootPath, getManifestPath(req.url)));
if (!absoluteJoinedPath.startsWith(absoluteRootPath)) {
res.statusCode = 500;
res.end('Error: Detected directory traversal');
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be best to just disallow ../ in the requests? If we just want to go with that logic, then we could probably simplify it by doing it using path.relative? WDYT?

for (const rootPath of this._rootPaths) {
  const relativePath = path.relative(rootPath, path.join(rootPath, req.url));
  if (relativePath.startsWith('../')) { throw }
}

Copy link
Member Author

@jelbourn jelbourn Feb 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the approach recommended by Google's security team. As I see it, it's safer because it ensures that, at the very end, the resolved URL didn't escape the webserver root.

My first approach wouldn't have caught all cases (https://wikipedia.org/wiki/Directory_traversal_attack#Variations_of_directory_traversal), and is more future proof in case mre ways of directory traversal are introduced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that sounds reasonable.

I'm unsure about path.resolve though. The root paths are not file system paths in the dev-server, but rather Bazel manifest/runfile paths, so resolving paths from the cwd is incorrect. It doesn't impact the URL escape check in general, but it's technically not correct. It might be better to use path.relative to determine if the paths escapes the root path.

// Implements the HTML history API fallback logic based on the requirements of the
// "connect-history-api-fallback" package. See the conditions for a request being redirected
// to the index: https://github.com/bripkens/connect-history-api-fallback#introduction
Expand All @@ -84,15 +95,19 @@ export class DevServer {

/** Resolves a given URL from the runfiles using the corresponding manifest path. */
private _resolveUrlFromRunfiles(url: string): string|null {
// Remove the leading slash from the URL. Manifest paths never
// start with a leading slash.
const manifestPath = url.substring(1);
for (let rootPath of this._rootPaths) {
try {
return require.resolve(path.posix.join(rootPath, manifestPath));
return require.resolve(path.posix.join(rootPath, getManifestPath(url)));
} catch {
}
}
return null;
}
}

/** Gets the manifest path for a given url */
function getManifestPath(url: string) {
// Remove the leading slash from the URL. Manifest paths never
// start with a leading slash.
return url.substring(1);
}