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

fix: vulnerability email #256

Merged
merged 5 commits into from
Jan 23, 2025
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
4 changes: 3 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"date-fns": "^2.30.0",
"date-fns-tz": "^1.3.7",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.5.0",
"helmet": "^4.0.0",
"morgan": "^1.10.0",
"node-cron": "^3.0.2",
Expand All @@ -46,4 +48,4 @@
"node": ">= 14"
},
"packageManager": "yarn@3.6.4"
}
}
6 changes: 5 additions & 1 deletion api/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ const DATABASE_URL = process.env.DATABASE_URL;
const CRONJOBS_ENABLED = process.env.CRONJOBS_ENABLED === "true";

const PUSH_NOTIFICATION_GCM_ID = process.env.PUSH_NOTIFICATION_GCM_ID;
const PUSH_NOTIFICATION_APN_KEY = process.env.PUSH_NOTIFICATION_APN_KEY.replace(/\\n/g, "\n");
const PUSH_NOTIFICATION_APN_KEY = process.env.PUSH_NOTIFICATION_APN_KEY?.replace(/\\n/g, "\n");
const PUSH_NOTIFICATION_APN_KEY_ID = process.env.PUSH_NOTIFICATION_APN_KEY_ID;
const PUSH_NOTIFICATION_APN_TEAM_ID = process.env.PUSH_NOTIFICATION_APN_TEAM_ID;

const TIPIMAIL_API_KEY = process.env.TIPIMAIL_API_KEY;
const TIPIMAIL_API_USER = process.env.TIPIMAIL_API_USER;

const HMAC_SECRET = process.env.HMAC_SECRET;

if (process.env.NODE_ENV === "development") {
console.log("✍️ ~CONFIG ", {
PORT,
Expand All @@ -37,6 +39,7 @@ if (process.env.NODE_ENV === "development") {
CRONJOBS_ENABLED,
TIPIMAIL_API_KEY,
TIPIMAIL_API_USER,
HMAC_SECRET,
});
}

Expand All @@ -57,4 +60,5 @@ module.exports = {
PUSH_NOTIFICATION_APN_TEAM_ID,
TIPIMAIL_API_KEY,
TIPIMAIL_API_USER,
HMAC_SECRET,
};
6 changes: 5 additions & 1 deletion api/src/controllers/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ const { TIPIMAIL_API_USER, TIPIMAIL_API_KEY, ENVIRONMENT } = require("../config"
const { catchErrors } = require("../middlewares/errors");
const router = express.Router();
const { capture } = require("../third-parties/sentry");
const { validateHMAC } = require("../middlewares/hmac");
const { mailLimiter } = require("../middlewares/rateLimit");

router.post(
"/",
validateHMAC,
mailLimiter,
catchErrors(async (req, res) => {
let { to, replyTo, replyToName, subject, text, html } = req.body || {};

if (!subject || (!text && !html)) return res.status(400).json({ ok: false, error: "wrong parameters" });

if (!to) {
to = ENVIRONMENT === "development" ? "tangimds@gmail.com" : "jardinmental@fabrique.social.gouv.fr";
to = ENVIRONMENT === "development" ? process.env.MAIL_TO_DEV : "jardinmental@fabrique.social.gouv.fr";
}

if (!replyTo) {
Expand Down
5 changes: 4 additions & 1 deletion api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ app.get("/config", async (req, res) => {
// hello world
const now = new Date();
app.get("/", async (req, res) => {
res.send(`api MSP • ${now.toISOString()}`);
res.send({
name: "api jardin mental",
last_deployed_at: now.toISOString(),
});
});

// Add header with API version to compare with client.
Expand Down
32 changes: 32 additions & 0 deletions api/src/middlewares/hmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const crypto = require("crypto");
const { HMAC_SECRET } = require("../config");

const validateHMAC = (req, res, next) => {
const secret = HMAC_SECRET;
if (!secret) {
return next();
}
const { "x-signature": signature, "x-timestamp": timestamp } = req.headers;

if (!signature || !timestamp) {
return res.status(400).json({ error: "Missing signature or timestamp" });
}

const now = Date.now();
if (Math.abs(now - timestamp) > 5 * 60 * 1000) {
// Vérifie un délai de 5 minutes
return res.status(400).json({ error: "Timestamp expired" });
}

const payload = JSON.stringify(req.body);
const dataToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto.createHmac("sha256", secret).update(dataToSign).digest("hex");

if (signature !== expectedSignature) {
return res.status(401).json({ error: "Invalid signature" });
}

next();
};

module.exports = { validateHMAC };
25 changes: 25 additions & 0 deletions api/src/middlewares/rateLimit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const rateLimit = require("express-rate-limit");

// Middleware de rate limiting pour la route /reminder
const reminderLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // Fenêtre de 15 minutes
max: 10, // Maximum de 10 requêtes dans cette période
keyGenerator: (req) => req.body.pushNotifToken || req.ip, // Limite basée sur pushNotifToken ou IP
message: {
ok: false,
error: "Too many requests. Please try again later.",
},
});

// Middleware de rate limiting pour la route /mail
const mailLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // Fenêtre de 1 minute
max: 5, // Maximum de 5 mails par minute
keyGenerator: (req) => req.ip, // Limite basée sur l'IP uniquement
message: {
ok: false,
error: "Too many emails sent. Please wait a moment and try again.",
},
});

module.exports = { reminderLimiter, mailLimiter };
18 changes: 18 additions & 0 deletions api/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,12 @@ __metadata:
bcryptjs: ^2.4.3
cors: ^2.8.5
cross-env: ^7.0.3
crypto-js: ^4.2.0
date-fns: ^2.30.0
date-fns-tz: ^1.3.7
dotenv: ^16.3.1
express: ^4.18.2
express-rate-limit: ^7.5.0
helmet: ^4.0.0
morgan: ^1.10.0
node-cron: ^3.0.2
Expand Down Expand Up @@ -841,6 +843,13 @@ __metadata:
languageName: node
linkType: hard

"crypto-js@npm:^4.2.0":
version: 4.2.0
resolution: "crypto-js@npm:4.2.0"
checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774
languageName: node
linkType: hard

"crypto-random-string@npm:^2.0.0":
version: 2.0.0
resolution: "crypto-random-string@npm:2.0.0"
Expand Down Expand Up @@ -1105,6 +1114,15 @@ __metadata:
languageName: node
linkType: hard

"express-rate-limit@npm:^7.5.0":
version: 7.5.0
resolution: "express-rate-limit@npm:7.5.0"
peerDependencies:
express: ^4.11 || 5 || ^5.0.0-beta.1
checksum: 2807341039c111eed292e28768aff3c69515cb96ff15799976a44ead776c41931d6947fe3da3cea021fa0490700b1ab468b4832bbed7d231bed63c195d22b959
languageName: node
linkType: hard

"express@npm:^4.18.2":
version: 4.18.2
resolution: "express@npm:4.18.2"
Expand Down
14 changes: 4 additions & 10 deletions app/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,24 @@
"distribution": "internal",
"env": {
"EXPO_PUBLIC_SCHEME": "http",
"EXPO_PUBLIC_HOST": "localhost",
"EXPO_PUBLIC_APP_ENV": "development",
"EXPO_PUBLIC_TIPIMAIL_API_KEY": "1234567890",
"EXPO_PUBLIC_TIPIMAIL_API_USER": "1234567890"
"EXPO_PUBLIC_HOST": "localhost:3000",
"EXPO_PUBLIC_APP_ENV": "development"
}
},
"preview": {
"distribution": "internal",
"env": {
"EXPO_PUBLIC_SCHEME": "https",
"EXPO_PUBLIC_HOST": "api-monsuivipsy.fabrique.social.gouv.fr",
"EXPO_PUBLIC_APP_ENV": "production",
"EXPO_PUBLIC_TIPIMAIL_API_KEY": "1234567890",
"EXPO_PUBLIC_TIPIMAIL_API_USER": "1234567890"
"EXPO_PUBLIC_APP_ENV": "production"
}
},
"production": {
"autoIncrement": true,
"env": {
"EXPO_PUBLIC_SCHEME": "https",
"EXPO_PUBLIC_HOST": "api-monsuivipsy.fabrique.social.gouv.fr",
"EXPO_PUBLIC_APP_ENV": "production",
"EXPO_PUBLIC_TIPIMAIL_API_KEY": "1234567890",
"EXPO_PUBLIC_TIPIMAIL_API_USER": "1234567890"
"EXPO_PUBLIC_APP_ENV": "production"
},
"android": {
"credentialsSource": "local"
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@react-navigation/material-top-tabs": "^6.6.14",
"@react-navigation/native": "^6.1.18",
"@react-navigation/stack": "^6.4.1",
"crypto-js": "^4.2.0",
"csv-parser": "^3.0.0",
"date-fns": "^2.16.1",
"dayjs": "^1.11.3",
Expand Down
9 changes: 3 additions & 6 deletions app/src/config.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
// import envConfig from "react-native-config";
import {version, buildNumber} from '../package.json';

// const SCHEME = envConfig.SCHEME;
// const HOST = envConfig.HOST;
// const APP_ENV = envConfig.APP_ENV;
const SCHEME = process.env.EXPO_PUBLIC_SCHEME;
const HOST = process.env.EXPO_PUBLIC_HOST;
const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV;
const VERSION = version;
const BUILD_NUMBER = buildNumber;
// const TIPIMAIL_API_KEY = envConfig.TIPIMAIL_API_KEY;
// const TIPIMAIL_API_USER = envConfig.TIPIMAIL_API_USER;

export {SCHEME, HOST, APP_ENV, VERSION, BUILD_NUMBER};
const HMAC_SECRET = process.env.EXPO_PUBLIC_HMAC_SECRET;

export {SCHEME, HOST, APP_ENV, VERSION, BUILD_NUMBER, HMAC_SECRET};
4 changes: 3 additions & 1 deletion app/src/scenes/presentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const PdfViewer = ({navigation}) => {
<Text className="text-gray-700 text-sm">Retour</Text>
</Pressable>
</View>
<WebView style={{width: '100%', height: '100%'}} source={{uri}} originWhitelist={['*']} javaScriptEnabled={true} domStorageEnabled={true} />
<View className="flex-1">
<WebView style={{width: '100%', height: '100%'}} source={{uri}} originWhitelist={['*']} javaScriptEnabled={true} domStorageEnabled={true} />
</View>
</SafeAreaView>
);
};
Expand Down
47 changes: 28 additions & 19 deletions app/src/services/api.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import URI from "urijs";
import { Alert, Linking, Platform } from "react-native";
import NetInfo from "@react-native-community/netinfo";
import fetchRetry from "fetch-retry";
import URI from 'urijs';
import {Alert, Linking, Platform} from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import fetchRetry from 'fetch-retry';

import { SCHEME, HOST, BUILD_NUMBER } from "../config";
import matomo from "./matomo";
import {SCHEME, HOST, BUILD_NUMBER} from '../config';
import matomo from './matomo';
import {generateHMAC} from '../utils/generateHmac';

export const checkNetwork = async (test = false) => {
const isConnected = await NetInfo.fetch().then((state) => state.isConnected);
const isConnected = await NetInfo.fetch().then(state => state.isConnected);
if (!isConnected || test) {
await new Promise((res) => setTimeout(res, 1500));
Alert.alert("Pas de réseau", "Veuillez vérifier votre connexion");
await new Promise(res => setTimeout(res, 1500));
Alert.alert('Pas de réseau', 'Veuillez vérifier votre connexion');
return false;
}
return true;
Expand All @@ -23,13 +24,21 @@ class ApiService {
getUrl = (path, query) => {
return new URI().host(this.host).scheme(this.scheme).path(path).setSearch(query).toString();
};
execute = async ({ method = "GET", path = "", query = {}, headers = {}, body = null }) => {
execute = async ({method = 'GET', path = '', query = {}, headers = {}, body = null}) => {
try {
if (body) {
const {signature, timestamp} = generateHMAC(body);
headers = {
...headers,
'x-signature': signature,
'x-timestamp': timestamp,
};
}
const config = {
method,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
'Content-Type': 'application/json',
Accept: 'application/json',
appversion: BUILD_NUMBER,
appdevice: Platform.OS,
currentroute: this.navigation?.getCurrentRoute?.()?.name,
Expand All @@ -41,7 +50,7 @@ class ApiService {
};

const url = this.getUrl(path, query);
console.log("api: ", { url });
// console.log('api: ', {url});
const canFetch = await checkNetwork();
if (!canFetch) return;

Expand All @@ -65,15 +74,15 @@ class ApiService {

needUpdate = false;

get = async (args) => this.execute({ method: "GET", ...args });
post = async (args) => this.execute({ method: "POST", ...args });
put = async (args) => this.execute({ method: "PUT", ...args });
delete = async (args) => this.execute({ method: "DELETE", ...args });
get = async args => this.execute({method: 'GET', ...args});
post = async args => this.execute({method: 'POST', ...args});
put = async args => this.execute({method: 'PUT', ...args});
delete = async args => this.execute({method: 'DELETE', ...args});

handleInAppMessage = (inAppMessage) => {
handleInAppMessage = inAppMessage => {
const [title, subTitle, actions = [], options = {}] = inAppMessage;
if (!actions || !actions.length) return Alert.alert(title, subTitle);
const actionsWithNavigation = actions.map((action) => {
const actionsWithNavigation = actions.map(action => {
if (action.navigate) {
action.onPress = () => {
API.navigation.navigate(...action.navigate);
Expand Down
10 changes: 10 additions & 0 deletions app/src/utils/generateHmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import crypto from "crypto-js";
import { HMAC_SECRET } from "../config";

export const generateHMAC = (payload) => {
const timestamp = Date.now().toString();
const dataToSign = `${timestamp}:${JSON.stringify(payload)}`;
const signature = crypto.HmacSHA256(dataToSign, HMAC_SECRET).toString();

return { signature, timestamp };
};
Loading
Loading