Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shim internet access out of extensions on extension host and in web view #93

Merged
merged 11 commits into from
Mar 20, 2023
Merged
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.17.1
18.0.0
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"endregion",
"guids",
"Hopkinson",
"iframes",
"localstorage",
"maximizable",
"nums",
Expand Down
49 changes: 49 additions & 0 deletions extensions/evil/evil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable global-require */

'use strict';

// eslint-disable-next-line import/no-unresolved
const papi = require('papi');

const { logger } = papi;

logger.log('Evil is importing! Mwahahaha');

try {
// This will be blocked
const fs = require('fs');
logger.log(`Successfully imported fs! fs.readFileSync = ${fs.readFileSync}`);
} catch (e) {
logger.log(e.message);
}

try {
// This will be blocked and will suggest the papi.fetch api
const https = require('https');
logger.log(`Successfully imported https! ${https}`);
} catch (e) {
logger.log(e.message);
}

try {
// This is just for testing and will throw an exception
fetch('test');
} catch (e) {
logger.log(`Evil: Error on fetch! ${e}`);
}

try {
// This is just for testing and will throw an exception
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const xhr = new XMLHttpRequest();
} catch (e) {
logger.log(`Evil: Error on XMLHttpRequest! ${e}`);
}

try {
// This is just for testing and will throw an exception
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const webSocket = new WebSocket();
} catch (e) {
logger.log(`Evil: Error on WebSocket! ${e}`);
}
10 changes: 10 additions & 0 deletions extensions/evil/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "evil",
"version": "0.0.1",
"description": "Paranext extension that tries to break things!!1!!",
"author": "TJ Couch",
"license": "MIT",
"main": "evil.js",
"activationEvents": [
]
}
11 changes: 11 additions & 0 deletions extensions/evil/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "evil",
"version": "0.0.1",
"description": "Paranext extension that tries to break things!!1!!",
"main": "evil.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "TJ Couch",
"license": "MIT"
}
9 changes: 0 additions & 9 deletions extensions/hello-someone/hello-someone.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ const { logger } = papi;

logger.log('Hello Someone is importing!');

// This will be blocked
const fs = require('fs');

logger.log(
fs.message
? fs.message
: `Successfully imported fs! fs.readFileSync = ${fs.readFileSync}`,
);

const unsubscribers = [];

exports.activate = async () => {
Expand Down
46 changes: 35 additions & 11 deletions extensions/hello-world/hello-world.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ const { logger } = papi;

logger.log('Hello world is importing!');

// This will be blocked
const fs = require('fs');

logger.log(
fs.message
? fs.message
: `Successfully imported fs! fs.readFileSync = ${fs.readFileSync}`,
);

const unsubscribers = [];

/** Gets the code to make the Hello World React component. Provide a name to use to identify this component. Provide a string to modify the 'function HelloWorld()' line */
Expand All @@ -30,6 +21,15 @@ const getReactComponent = (name, functionModifier = '') =>
logger
} = papi;

// Test fetching
papi
.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log(scr.text.replace(/\\n/g, '')))
.catch((e) =>
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);

${functionModifier} function HelloWorld() {
const test = useContext(TestContext) || 'Context didnt work!! :(';

Expand All @@ -51,6 +51,12 @@ const getReactComponent = (name, functionModifier = '') =>
onClick: () => {
logger.log('${name} PButton clicked!');
setMyState(myStateCurrent => myStateCurrent + 1);
papi.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log('Got it! ' + scr.text.replace(/\\n/g, '')))
.catch((e) =>
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);
}
},
'Hello World PButton ',
Expand Down Expand Up @@ -89,8 +95,16 @@ exports.activate = async () => {
}),
];

papi
.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log(scr.text.replace(/\n/g, '')))
.catch((e) =>
logger.error(`Could not get Scripture from bible-api! Reason: ${e}`),
);

papi.webViews.addWebView({
hasReact: false,
contentType: 'html',
contents: `<html>
<head>
</head>
Expand Down Expand Up @@ -147,14 +161,24 @@ exports.activate = async () => {
const container = document.getElementById('root');
const root = createRoot(container);
root.render(React.createElement(HelloWorld, null));

// Test fetching
papi
.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log(scr.text.replace(/\\n/g, '')))
.catch((e) =>
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);
});
</script>
</body>
</html>`,
});

papi.webViews.addWebView({
contents: getReactComponent('Hello World React Webview', 'export default'),
componentName: 'HelloWorld',
contents: getReactComponent('Hello World React Webview'),
});

return Promise.all(
Expand Down
30 changes: 9 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
"rc-dock": "^3.2.17",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-frame-component": "^5.2.6",
"react-router-dom": "^6.9.0",
"ws": "^8.13.0"
},
Expand Down
45 changes: 38 additions & 7 deletions src/extension-host/services/ExtensionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getPathFromUri, joinUriPaths } from '@node/util/util';
import { Uri } from '@shared/data/FileSystemTypes';
import { UnsubscriberAsync } from '@shared/util/PapiUtil';
import Module from 'module';
import papi from '@shared/services/papi';
import papi, { MODULE_SIMILAR_APIS } from '@shared/services/papi';
import logger from '@shared/util/logger';

/** Whether this service has finished setting up */
Expand Down Expand Up @@ -142,12 +142,37 @@ const activateExtensions = async (
}

// Disallow any imports within the extension
const message = `Requiring other than papi is not allowed in extensions! Rejected require('${fileName}')`;
return {
message,
};
// Tell the extension dev if there is an api similar to what they want to import
const similarApi =
MODULE_SIMILAR_APIS[fileName] || MODULE_SIMILAR_APIS[`node:${fileName}`];
const message = `Requiring other than papi is not allowed in extensions! Rejected require('${fileName}').${
similarApi ? ` Try using papi.${similarApi}` : ''
}`;
throw new Error(message);
}) as typeof Module.prototype.require;

// Shim out internet access options in environments where they are defined so extensions can't use them
const fetchOriginal: typeof fetch | undefined = globalThis.fetch;
// eslint-disable-next-line no-global-assign
globalThis.fetch = function fetchForbidden() {
throw Error('Cannot use fetch! Try using papi.fetch');
};

const xmlHttpRequestOriginal: typeof XMLHttpRequest | undefined =
globalThis.XMLHttpRequest;
// @ts-expect-error we want to remove XMLHttpRequest
// eslint-disable-next-line no-global-assign
globalThis.XMLHttpRequest = function XMLHttpRequestForbidden() {
throw Error('Cannot use XMLHttpRequest! Try using papi.fetch');
};

const webSocketOriginal: typeof WebSocket | undefined = globalThis.WebSocket;
// @ts-expect-error we want to remove WebSocket
// eslint-disable-next-line no-global-assign
globalThis.WebSocket = function WebSocketForbidden() {
throw Error('Cannot use WebSocket!');
};

// Import the extensions and run their activate() functions
const extensionsActive = (
await Promise.all(
Expand All @@ -165,9 +190,15 @@ const activateExtensions = async (
)
).filter((activeExtension) => activeExtension !== null) as ActiveExtension[];

// Put require back so we can use it again
// TODO: this probably lets extensions wait and require code later. Security concern. Pls fix
// Put shimmed out modules and globals back so we can use them again
// TODO: replacing the original modules and globals almost confidently lets extensions wait and use them later. Serious security concern. Pls fix
Module.prototype.require = requireOriginal;
// eslint-disable-next-line no-global-assign
globalThis.fetch = fetchOriginal;
// eslint-disable-next-line no-global-assign
globalThis.XMLHttpRequest = xmlHttpRequestOriginal;
// eslint-disable-next-line no-global-assign
globalThis.WebSocket = webSocketOriginal;

return extensionsActive;
};
Expand Down
Loading