Skip to content

Commit

Permalink
Shim internet access out of extensions on extension host and in web v…
Browse files Browse the repository at this point in the history
…iew (#93)
  • Loading branch information
tjcouch-sil authored Mar 20, 2023
2 parents f204ed3 + 3710c76 commit eb012b5
Show file tree
Hide file tree
Showing 23 changed files with 430 additions and 116 deletions.
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

0 comments on commit eb012b5

Please sign in to comment.