Skip to content
Open
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
1 change: 1 addition & 0 deletions app/src/function_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum FunctionId {
FnAppCheckGetTokenAsync,
FnAppCheckAddListener,
FnAppCheckRemoveListener,
FnAppCheckGetLimitedUseTokenAsync,
};

// Class for providing a generic way for firebase libraries to expose their
Expand Down
1 change: 1 addition & 0 deletions app_check/src/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum AppCheckFn {
kAppCheckFnGetAppCheckToken = 0,
kAppCheckFnGetAppCheckStringInternal,
kAppCheckFnGetLimitedUseAppCheckToken,
kAppCheckFnGetLimitedUseAppCheckStringInternal,
kAppCheckFnCount,
};

Expand Down
47 changes: 47 additions & 0 deletions app_check/src/desktop/app_check_desktop.cc
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,31 @@ Future<std::string> AppCheckInternal::GetAppCheckTokenStringInternal() {
return MakeFuture(future(), handle);
}

Future<std::string>
AppCheckInternal::GetLimitedUseAppCheckTokenStringInternal() {
auto handle = future()->SafeAlloc<std::string>(
kAppCheckFnGetLimitedUseAppCheckStringInternal);

AppCheckProvider* provider = GetProvider();
if (provider != nullptr) {
auto token_callback{[this, handle](firebase::app_check::AppCheckToken token,
int error_code,
const std::string& error_message) {
if (error_code == firebase::app_check::kAppCheckErrorNone) {
future()->CompleteWithResult(handle, 0, token.token);
} else {
future()->Complete(handle, error_code, error_message.c_str());
}
}};
provider->GetLimitedUseToken(token_callback);
} else {
future()->Complete(handle,
firebase::app_check::kAppCheckErrorInvalidConfiguration,
"No AppCheckProvider installed.");
}
return MakeFuture(future(), handle);
}

void AppCheckInternal::AddAppCheckListener(AppCheckListener* listener) {
if (listener) {
token_listeners_.push_back(listener);
Expand All @@ -211,6 +236,9 @@ void AppCheckInternal::InitRegistryCalls() {
app_->function_registry()->RegisterFunction(
::firebase::internal::FnAppCheckGetTokenAsync,
AppCheckInternal::GetAppCheckTokenAsyncForRegistry);
app_->function_registry()->RegisterFunction(
::firebase::internal::FnAppCheckGetLimitedUseTokenAsync,
AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry);
app_->function_registry()->RegisterFunction(
::firebase::internal::FnAppCheckAddListener,
AppCheckInternal::AddAppCheckListenerForRegistry);
Expand All @@ -226,6 +254,8 @@ void AppCheckInternal::CleanupRegistryCalls() {
if (g_app_check_registry_count == 0) {
app_->function_registry()->UnregisterFunction(
::firebase::internal::FnAppCheckGetTokenAsync);
app_->function_registry()->UnregisterFunction(
::firebase::internal::FnAppCheckGetLimitedUseTokenAsync);
app_->function_registry()->UnregisterFunction(
::firebase::internal::FnAppCheckAddListener);
app_->function_registry()->UnregisterFunction(
Expand All @@ -250,6 +280,23 @@ bool AppCheckInternal::GetAppCheckTokenAsyncForRegistry(App* app,
return false;
}

// static
bool AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry(
App* app, void* /*unused*/, void* out) {
Future<std::string>* out_future = static_cast<Future<std::string>*>(out);
if (!app || !out_future) {
return false;
}

AppCheck* app_check = AppCheck::GetInstance(app);
if (app_check && app_check->internal_) {
*out_future =
app_check->internal_->GetLimitedUseAppCheckTokenStringInternal();
return true;
}
return false;
}

void FunctionRegistryAppCheckListener::AddListener(
FunctionRegistryCallback callback, void* context) {
callbacks_.emplace_back(callback, context);
Expand Down
8 changes: 8 additions & 0 deletions app_check/src/desktop/app_check_desktop.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class AppCheckInternal {
// internal methods to not conflict with the publicly returned future.
Future<std::string> GetAppCheckTokenStringInternal();

// Gets the limited-use App Check token as just the string, to be used by
// internal methods to not conflict with the publicly returned future.
Future<std::string> GetLimitedUseAppCheckTokenStringInternal();

void AddAppCheckListener(AppCheckListener* listener);

void RemoveAppCheckListener(AppCheckListener* listener);
Expand Down Expand Up @@ -100,6 +104,10 @@ class AppCheckInternal {
static bool GetAppCheckTokenAsyncForRegistry(App* app, void* /*unused*/,
void* out_future);

static bool GetLimitedUseAppCheckTokenAsyncForRegistry(App* app,
void* /*unused*/,
void* out_future);

static bool AddAppCheckListenerForRegistry(App* app, void* callback,
void* context);

Expand Down
50 changes: 50 additions & 0 deletions functions/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,54 @@ TEST_F(FirebaseFunctionsTest, TestFunctionFromURL) {
EXPECT_EQ(result.map()["operationResult"], 6);
}

TEST_F(FirebaseFunctionsTest, TestFunctionWithLimitedUseAppCheckToken) {
SignIn();

// addNumbers(5, 7) = 12
firebase::Variant data(firebase::Variant::EmptyMap());
data.map()["firstNumber"] = 5;
data.map()["secondNumber"] = 7;

firebase::functions::HttpsCallableOptions options;
options.limited_use_app_check_token = true;

LogDebug("Calling addNumbers with Limited Use App Check Token");
firebase::functions::HttpsCallableReference ref =
functions_->GetHttpsCallable("addNumbers", options);

firebase::Variant result =
TestFunctionHelper("addNumbers", ref, &data, firebase::Variant::Null())
.result()
->data();
EXPECT_TRUE(result.is_map());
EXPECT_EQ(result.map()["operationResult"], 12);
}

TEST_F(FirebaseFunctionsTest, TestFunctionFromURLWithLimitedUseAppCheckToken) {
SignIn();

// addNumbers(4, 2) = 6
firebase::Variant data(firebase::Variant::EmptyMap());
data.map()["firstNumber"] = 4;
data.map()["secondNumber"] = 2;

std::string proj = app_->options().project_id();
std::string url =
"https://us-central1-" + proj + ".cloudfunctions.net/addNumbers";

firebase::functions::HttpsCallableOptions options;
options.limited_use_app_check_token = true;

LogDebug("Calling by URL %s with Limited Use App Check Token", url.c_str());
firebase::functions::HttpsCallableReference ref =
functions_->GetHttpsCallableFromURL(url.c_str(), options);

firebase::Variant result =
TestFunctionHelper(url.c_str(), ref, &data, firebase::Variant::Null())
.result()
->data();
EXPECT_TRUE(result.is_map());
EXPECT_EQ(result.map()["operationResult"], 6);
}

} // namespace firebase_testapp_automated
90 changes: 83 additions & 7 deletions functions/src/android/functions_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ namespace internal {
"(Ljava/lang/String;)" \
"Lcom/google/firebase/functions/HttpsCallableReference;", \
util::kMethodTypeInstance), \
X(GetHttpsCallableWithOptions, "getHttpsCallable", \
"(Ljava/lang/String;Lcom/google/firebase/functions/HttpsCallableOptions;)" \
"Lcom/google/firebase/functions/HttpsCallableReference;", \
util::kMethodTypeInstance), \
X(GetHttpsCallableFromURL, "getHttpsCallableFromUrl", \
"(Ljava/net/URL;)" \
"Lcom/google/firebase/functions/HttpsCallableReference;", \
util::kMethodTypeInstance), \
X(GetHttpsCallableFromURLWithOptions, "getHttpsCallableFromUrl", \
"(Ljava/net/URL;Lcom/google/firebase/functions/HttpsCallableOptions;)" \
"Lcom/google/firebase/functions/HttpsCallableReference;", \
util::kMethodTypeInstance), \
X(UseFunctionsEmulator, "useFunctionsEmulator", \
"(Ljava/lang/String;)V", \
util::kMethodTypeInstance)
Expand All @@ -50,6 +58,21 @@ METHOD_LOOKUP_DEFINITION(firebase_functions,
"com/google/firebase/functions/FirebaseFunctions",
FIREBASE_FUNCTIONS_METHODS)

// clang-format off
#define CALLABLE_OPTIONS_METHODS(X) \
X(BuilderConstructor, "<init>", "()V"), \
X(SetLimitedUseAppCheckTokens, "setLimitedUseAppCheckTokens", \
"(Z)Lcom/google/firebase/functions/HttpsCallableOptions$Builder;"), \
X(Build, "build", "()Lcom/google/firebase/functions/HttpsCallableOptions;")
// clang-format on

METHOD_LOOKUP_DECLARATION(callable_options_builder, CALLABLE_OPTIONS_METHODS)
METHOD_LOOKUP_DEFINITION(
callable_options_builder,
PROGUARD_KEEP_CLASS
"com/google/firebase/functions/HttpsCallableOptions$Builder",
CALLABLE_OPTIONS_METHODS)

// clang-format off
#define FUNCTIONS_EXCEPTION_METHODS(X) \
X(GetMessage, "getMessage", "()Ljava/lang/String;"), \
Expand Down Expand Up @@ -125,6 +148,7 @@ bool FunctionsInternal::Initialize(App* app) {
functions_exception::CacheMethodIds(env, activity) &&
functions_exception_code::CacheMethodIds(env, activity) &&
functions_exception_code::CacheFieldIds(env, activity) &&
callable_options_builder::CacheMethodIds(env, activity) &&
// Call Initialize on all other Functions internal classes.
HttpsCallableReferenceInternal::Initialize(app))) {
return false;
Expand All @@ -144,6 +168,7 @@ void FunctionsInternal::Terminate(App* app) {
firebase_functions::ReleaseClass(env);
functions_exception::ReleaseClass(env);
functions_exception_code::ReleaseClass(env);
callable_options_builder::ReleaseClass(env);

// Call Terminate on all other Functions internal classes.
HttpsCallableReferenceInternal::Terminate(app);
Expand Down Expand Up @@ -186,14 +211,40 @@ Error FunctionsInternal::ErrorFromJavaFunctionsException(

HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable(
const char* name) const {
return GetHttpsCallable(name, HttpsCallableOptions());
}

HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable(
const char* name, const HttpsCallableOptions& options) const {
FIREBASE_ASSERT_RETURN(nullptr, name != nullptr);
JNIEnv* env = app_->GetJNIEnv();

// Create HttpsCallableOptions
jobject builder =
env->NewObject(callable_options_builder::GetClass(),
callable_options_builder::GetMethodId(
callable_options_builder::kBuilderConstructor));
jobject builder2 = env->CallObjectMethod(
builder,
callable_options_builder::GetMethodId(
callable_options_builder::kSetLimitedUseAppCheckTokens),
options.limited_use_app_check_token);
env->DeleteLocalRef(builder);
builder = builder2;
jobject java_options = env->CallObjectMethod(
builder,
callable_options_builder::GetMethodId(callable_options_builder::kBuild));
env->DeleteLocalRef(builder);

jobject name_string = env->NewStringUTF(name);
jobject callable_reference_obj = env->CallObjectMethod(
obj_,
firebase_functions::GetMethodId(firebase_functions::kGetHttpsCallable),
name_string);
firebase_functions::GetMethodId(
firebase_functions::kGetHttpsCallableWithOptions),
name_string, java_options);
env->DeleteLocalRef(name_string);
env->DeleteLocalRef(java_options);

if (util::LogException(env, kLogLevelError,
"Functions::GetHttpsCallable() (name = %s) failed",
name)) {
Expand All @@ -208,15 +259,40 @@ HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable(

HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL(
const char* url) const {
return GetHttpsCallableFromURL(url, HttpsCallableOptions());
}

HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL(
const char* url, const HttpsCallableOptions& options) const {
Comment on lines +265 to +266
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The Android implementation of GetHttpsCallableFromURL lacks validation for the provided URL string. It directly converts the input to a java.net.URL and uses it to create a reference that will include sensitive Auth and App Check tokens in its requests. This poses a risk of sensitive information leakage if the URL is user-controlled or uses an insecure protocol.

Recommendation: Implement validation to ensure the URL is secure (HTTPS) and points to a trusted destination.

FIREBASE_ASSERT_RETURN(nullptr, url != nullptr);
JNIEnv* env = app_->GetJNIEnv();

// Create HttpsCallableOptions
jobject builder =
env->NewObject(callable_options_builder::GetClass(),
callable_options_builder::GetMethodId(
callable_options_builder::kBuilderConstructor));
jobject builder2 = env->CallObjectMethod(
builder,
callable_options_builder::GetMethodId(
callable_options_builder::kSetLimitedUseAppCheckTokens),
options.limited_use_app_check_token);
env->DeleteLocalRef(builder);
builder = builder2;
jobject java_options = env->CallObjectMethod(
builder,
callable_options_builder::GetMethodId(callable_options_builder::kBuild));
env->DeleteLocalRef(builder);

jobject url_object = util::CharsToURL(env, url);
jobject callable_reference_obj =
env->CallObjectMethod(obj_,
firebase_functions::GetMethodId(
firebase_functions::kGetHttpsCallableFromURL),
url_object);
jobject callable_reference_obj = env->CallObjectMethod(
obj_,
firebase_functions::GetMethodId(
firebase_functions::kGetHttpsCallableFromURLWithOptions),
url_object, java_options);
env->DeleteLocalRef(url_object);
env->DeleteLocalRef(java_options);

if (util::LogException(
env, kLogLevelError,
"Functions::GetHttpsCallableFromURL() (url = %s) failed", url)) {
Expand Down
6 changes: 6 additions & 0 deletions functions/src/android/functions_android.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@ class FunctionsInternal {

HttpsCallableReferenceInternal* GetHttpsCallable(const char* name) const;

HttpsCallableReferenceInternal* GetHttpsCallable(
const char* name, const HttpsCallableOptions& options) const;

// Get a FunctionsReference for the specified URL.
HttpsCallableReferenceInternal* GetHttpsCallableFromURL(
const char* url) const;

HttpsCallableReferenceInternal* GetHttpsCallableFromURL(
const char* url, const HttpsCallableOptions& options) const;

void UseFunctionsEmulator(const char* origin);

// Convert an error code obtained from a Java FunctionsException into a C++
Expand Down
13 changes: 13 additions & 0 deletions functions/src/common/functions.cc
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,25 @@ HttpsCallableReference Functions::GetHttpsCallable(const char* name) const {
return HttpsCallableReference(internal_->GetHttpsCallable(name));
}

HttpsCallableReference Functions::GetHttpsCallable(
const char* name, const HttpsCallableOptions& options) const {
if (!internal_) return HttpsCallableReference();
return HttpsCallableReference(internal_->GetHttpsCallable(name, options));
}

HttpsCallableReference Functions::GetHttpsCallableFromURL(
const char* url) const {
if (!internal_) return HttpsCallableReference();
return HttpsCallableReference(internal_->GetHttpsCallableFromURL(url));
}

HttpsCallableReference Functions::GetHttpsCallableFromURL(
const char* url, const HttpsCallableOptions& options) const {
if (!internal_) return HttpsCallableReference();
return HttpsCallableReference(
internal_->GetHttpsCallableFromURL(url, options));
}

void Functions::UseFunctionsEmulator(const char* origin) {
if (!internal_) return;
internal_->UseFunctionsEmulator(origin);
Expand Down
Loading
Loading